mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Optimize terminal startup and memory use (#464)
- Reduce allocation overhead in array comparisons and string operations. - Reuse fiber stacks and release cached memory while idle. - Read terminal dimensions directly from native streams.
This commit is contained in:
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.15.0",
|
||||
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
|
||||
"artifactVersion": "05c048b58a8f3c257058",
|
||||
"artifactVersion": "eef8a83e26e3222956a5",
|
||||
"rowCount": 3662
|
||||
}
|
||||
|
||||
@@ -1130,9 +1130,9 @@
|
||||
"apiEntries": 18,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 3,
|
||||
"refused": 0,
|
||||
"not-implemented": 18,
|
||||
"not-implemented": 15,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -80782,7 +80782,7 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"status": "partial",
|
||||
"detail": "Derived from the exact API rows in this chapter.",
|
||||
"verification": "derived"
|
||||
},
|
||||
@@ -81146,9 +81146,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -81202,9 +81202,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"tiers": {
|
||||
"static": {
|
||||
"replace-refusal": 46,
|
||||
"verify-gap": 2581,
|
||||
"audit-partial": 585,
|
||||
"verify-gap": 2579,
|
||||
"audit-partial": 587,
|
||||
"classify": 2,
|
||||
"implement": 81
|
||||
},
|
||||
@@ -89736,12 +89736,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:tty",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:writeStream.columns",
|
||||
"tests": [
|
||||
"tests/corpus/1448-stream-columns.ts",
|
||||
"tests/corpus/2992-stream-rows.ts",
|
||||
"tests/harness/terminal-geometry.test.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -89802,12 +89806,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:tty",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:writeStream.rows",
|
||||
"tests": [
|
||||
"tests/corpus/1448-stream-columns.ts",
|
||||
"tests/corpus/2992-stream-rows.ts",
|
||||
"tests/harness/terminal-geometry.test.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
|
||||
@@ -1251,9 +1251,9 @@
|
||||
"apiEntries": 18,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 3,
|
||||
"refused": 0,
|
||||
"not-implemented": 18,
|
||||
"not-implemented": 15,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -106706,8 +106706,13 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "derived:descendants"
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/1448-stream-columns.ts",
|
||||
"tests/corpus/2992-stream-rows.ts",
|
||||
"tests/harness/terminal-geometry.test.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "partial",
|
||||
@@ -107066,8 +107071,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tty"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:writeStream.columns",
|
||||
"tests": [
|
||||
"tests/corpus/1448-stream-columns.ts",
|
||||
"tests/corpus/2992-stream-rows.ts",
|
||||
"tests/harness/terminal-geometry.test.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -107120,8 +107130,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tty"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:writeStream.rows",
|
||||
"tests": [
|
||||
"tests/corpus/1448-stream-columns.ts",
|
||||
"tests/corpus/2992-stream-rows.ts",
|
||||
"tests/harness/terminal-geometry.test.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
"node-builtin.path": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2915-path-function-values.ts", "tests/corpus/2916-path-function-values-cjs.ts", "tests/corpus/2917-builtin-callable-widths.ts", "tests/corpus/2918-builtin-callable-widths-cjs.ts"],
|
||||
"node-builtin.perf_hooks": ["tests/corpus/957-builtins-namespace.ts"],
|
||||
"node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"],
|
||||
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.process.exitCode": ["tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
|
||||
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
|
||||
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
|
||||
@@ -149,6 +151,7 @@
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }
|
||||
],
|
||||
"dedicated": [
|
||||
{ "symbols": ["writeStream.rows", "writeStream.columns"], "status": "partial", "evidence": ["tests/corpus/1448-stream-columns.ts", "tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"] },
|
||||
{ "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts"] },
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.doesNotReject", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts"] },
|
||||
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.timingSafeEqual", "crypto.randomFillSync", "crypto.randomInt", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts"] },
|
||||
|
||||
@@ -1729,9 +1729,9 @@ function emitContainerExpr(
|
||||
return emitter.newTemp(e.type, `scr_arr_state(${arr.name}, ${idx.name})`);
|
||||
}
|
||||
case "arrIntrinsic": {
|
||||
// getNumber copies out a scalar without invoking user code. A
|
||||
// getNumber/indexEq produce scalars without invoking user code. A
|
||||
// stable binding can own its receiver until that lookup finishes.
|
||||
const r = e.method === "getNumber"
|
||||
const r = e.method === "getNumber" || e.method === "indexEq"
|
||||
? emitStableReceiver(emitter, e.receiver, e.args)
|
||||
: emitter.emitExpr(e.receiver);
|
||||
if (e.receiver.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrIntrinsic on non-array");
|
||||
@@ -1749,6 +1749,12 @@ function emitContainerExpr(
|
||||
const start = emitter.emitExpr(e.args[0]!);
|
||||
return emitter.newTemp(e.type, `scr_arr_next_present(${r.name}, ${start.name})`);
|
||||
}
|
||||
case "indexEq": {
|
||||
const index = emitter.emitExpr(e.args[0]!);
|
||||
const other = emitStableReceiver(emitter, e.args[1]!, [e.args[2]!]);
|
||||
const otherIndex = emitter.emitExpr(e.args[2]!);
|
||||
return emitter.newTemp(e.type, `scr_arr_index_eq(${r.name}, ${index.name}, ${other.name}, ${otherIndex.name})`);
|
||||
}
|
||||
case "push": {
|
||||
// Variadic like JS: every argument evaluates first (left to
|
||||
// right — an argument reading the array sees the pre-push
|
||||
@@ -8066,22 +8072,23 @@ function emitProcessLibCall(state: LibCallState): Temp {
|
||||
return finish(`scr_stdin_next_chunk()`);
|
||||
case "process.isTTY":
|
||||
return finish(`scr_process_is_tty(${arg(0)})`);
|
||||
case "process.columns": {
|
||||
// ioctl(TIOCGWINSZ): a non-negative width wraps the f64 arm;
|
||||
case "process.columns":
|
||||
case "process.rows": {
|
||||
// ioctl(TIOCGWINSZ): a non-negative dimension wraps the f64 arm;
|
||||
// a non-TTY stream (or an ioctl refusal) comes back negative
|
||||
// and yields the interned undefined-arm instance — Node's
|
||||
// missing `.columns`. Type-directed union construction, like
|
||||
// missing geometry. Type-directed union construction, like
|
||||
// process.envGet.
|
||||
if (e.type.kind !== "union") {
|
||||
throw new InternalCompilerError("emitter bug: process.columns result is not a union");
|
||||
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
||||
}
|
||||
const def = emitter.unionsById.get(e.type.unionId);
|
||||
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
|
||||
const undefTag = undefinedArmTag(e.type, emitter.unionsById);
|
||||
if (f64Tag < 0 || undefTag < 0) {
|
||||
throw new InternalCompilerError("emitter bug: process.columns union lacks its arms");
|
||||
throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`);
|
||||
}
|
||||
const w = emitter.newTemp(F64, `scr_process_columns(${arg(0)})`);
|
||||
const w = emitter.newTemp(F64, `scr_process_${e.fn === "process.rows" ? "rows" : "columns"}(${arg(0)})`);
|
||||
const present = `scr_union_new_f64(${f64Tag}, ${w.name})`;
|
||||
const absent = emitter.unitInstanceRef(e.type.unionId, undefTag);
|
||||
return emitter.newTemp(e.type, `${w.name} >= 0 ? ${present} : ${absent}`);
|
||||
@@ -8951,6 +8958,14 @@ function emitIoLibCall(state: LibCallState): Temp {
|
||||
}
|
||||
|
||||
function emitLibCallExpr(emitter: CEmitter, e: LibCallExpr): Temp {
|
||||
const packed = e.args[0];
|
||||
if (e.fn === "string.fromCharCode" && packed?.kind === "arrayLit" &&
|
||||
packed.elems.length === 1 && !packed.spreads?.length) {
|
||||
// The literal is only the variadic ABI pack, never an observable
|
||||
// array. Evaluate the scalar once before bypassing that allocation.
|
||||
const code = emitter.emitExpr(packed.elems[0]!);
|
||||
return emitter.newTemp(e.type, `scr_str_from_char_code_one(${code.name})`);
|
||||
}
|
||||
// Standard-library call. Args are BORROWED (owned temps of the
|
||||
// current frame, released at statement end); refcounted results come
|
||||
// back +1 (process.argv: +1 on the runtime's ONE interned array —
|
||||
|
||||
@@ -203,9 +203,9 @@ export function emitStableReceiver(host: LlvmEmitterContext, receiver: IrExpr, f
|
||||
|
||||
export function emitArrIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: "arrIntrinsic" }): LlValue {
|
||||
const B = host.B;
|
||||
// getNumber copies out a scalar without invoking user code. A stable
|
||||
// getNumber/indexEq produce scalars without invoking user code. A stable
|
||||
// binding can own its receiver until that lookup finishes.
|
||||
const r = e.method === "getNumber"
|
||||
const r = e.method === "getNumber" || e.method === "indexEq"
|
||||
? host.emitStableReceiver(e.receiver, e.args)
|
||||
: host.emitExpr(e.receiver);
|
||||
if (e.receiver.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array");
|
||||
@@ -236,6 +236,15 @@ export function emitArrIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: "
|
||||
B.line(`${t} = call double @scr_arr_next_present(ptr ${r.name}, double ${start.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "indexEq": {
|
||||
const index = host.emitExpr(e.args[0]!);
|
||||
const other = host.emitStableReceiver(e.args[1]!, [e.args[2]!]);
|
||||
const otherIndex = host.emitExpr(e.args[2]!);
|
||||
host.declare(`declare zeroext i1 @scr_arr_index_eq(ptr, double, ptr, double)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call zeroext i1 @scr_arr_index_eq(ptr ${r.name}, double ${index.name}, ptr ${other.name}, double ${otherIndex.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "push": {
|
||||
// Variadic like JS: every argument evaluates first (left to
|
||||
// right), then each appends in order. Ownership of refcounted
|
||||
|
||||
@@ -658,6 +658,14 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
return host.wrapNullable(raw, raw, STRING, strTag, e.type, undefTag);
|
||||
}
|
||||
if (e.fn === "string.fromCharCode") {
|
||||
const packed = e.args[0]!;
|
||||
if (packed.kind === "arrayLit" && packed.elems.length === 1 && !packed.spreads?.length) {
|
||||
const code = host.emitExpr(packed.elems[0]!);
|
||||
host.declare(`declare ptr @scr_str_from_char_code_one(double)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_str_from_char_code_one(double ${code.name})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
// One packed f64[] (the frontend built it) or one bytes value (the
|
||||
// spread-typed-array form); +1 string.
|
||||
const sym = e.args[0]!.type.kind === "bytes" ? "scr_str_from_char_code_bytes" : "scr_str_from_char_code";
|
||||
|
||||
@@ -461,7 +461,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
B.line(`call void @scr_process_on_exit(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "process.envGet" || e.fn === "process.columns") {
|
||||
if (e.fn === "process.envGet" || e.fn === "process.columns" || e.fn === "process.rows") {
|
||||
// getenv(3) / ioctl(TIOCGWINSZ): the runtime answers a +1 string or
|
||||
// NULL (a width or a negative sentinel); the union construction is
|
||||
// type-directed HERE — present wraps the value arm, absent yields
|
||||
@@ -485,8 +485,9 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
B.line(`${raw} = call ptr @scr_env_get(ptr ${args[0]!.name})`);
|
||||
B.line(`${present} = icmp ne ptr ${raw}, null`);
|
||||
} else {
|
||||
host.declare(`declare double @scr_process_columns(double)`);
|
||||
B.line(`${raw} = call double @scr_process_columns(double ${args[0]!.name})`);
|
||||
const runtimeFn = e.fn === "process.rows" ? "scr_process_rows" : "scr_process_columns";
|
||||
host.declare(`declare double @${runtimeFn}(double)`);
|
||||
B.line(`${raw} = call double @${runtimeFn}(double ${args[0]!.name})`);
|
||||
B.line(`${present} = fcmp oge double ${raw}, ${f64Lit(0)}`);
|
||||
}
|
||||
B.condBr(present, lp, la);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BOOL, type IrExpr, type SrcLoc, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Fuse strict equality of two primitive array reads without optional-union
|
||||
* boxes. The arguments must be plain reads/literals: evaluating the right
|
||||
* receiver/index earlier must never mutate or invalidate the left value. */
|
||||
export function tryLowerIndexedComparison(
|
||||
lowerer: Lowerer, left: IrExpr, right: IrExpr, negated: boolean, loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
function indexed(expr: IrExpr) {
|
||||
if (expr.kind !== "call" || expr.args.length !== 2) return null;
|
||||
const [arr, index] = expr.args;
|
||||
if (arr?.type.kind !== "array" || index?.type.kind !== "f64") return null;
|
||||
const elem = arr.type.elem;
|
||||
if (elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool") return null;
|
||||
if (expr.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(elem)}`)) return null;
|
||||
if (arr.kind !== "varRef" || (index.kind !== "varRef" && index.kind !== "numLit")) return null;
|
||||
return { arr, index, elem };
|
||||
}
|
||||
const l = indexed(left), r = indexed(right);
|
||||
if (!l || !r || !typeEquals(l.elem, r.elem)) return null;
|
||||
const equal: IrExpr = {
|
||||
kind: "arrIntrinsic", method: "indexEq", receiver: l.arr,
|
||||
args: [l.index, r.arr, r.index], type: BOOL, loc,
|
||||
};
|
||||
return negated ? { kind: "unary", op: "!", operand: equal, type: BOOL, loc } : equal;
|
||||
}
|
||||
@@ -6896,7 +6896,7 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
|
||||
/** `process.stdin/stdout/stderr.isTTY` → isatty(3) on the stream's fd
|
||||
* (a REAL boolean: Node's non-TTY streams expose `undefined` here — the
|
||||
* documented divergence; truthiness tests, the actual usage, agree), and
|
||||
* `process.stdout/stderr.columns` → ioctl(TIOCGWINSZ) on the fd, with
|
||||
* `process.stdout/stderr.columns/rows` → ioctl(TIOCGWINSZ) on the fd, with
|
||||
* Node's non-TTY answer intact: the read is `number | undefined` and a
|
||||
* non-TTY (or ioctl-refusing) stream yields the undefined arm. The
|
||||
* receiver match sees through parens and as-casts to the SYMBOL —
|
||||
@@ -6910,7 +6910,7 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
|
||||
export function lowerProcessStreamProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.questionDotToken) return null;
|
||||
const member = expr.name.text;
|
||||
if (member !== "isTTY" && member !== "columns") return null;
|
||||
if (member !== "isTTY" && member !== "columns" && member !== "rows") return null;
|
||||
let recv: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) recv = recv.expression;
|
||||
// Browser-compatible packages commonly write
|
||||
@@ -6939,7 +6939,7 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
|
||||
if (member === "isTTY") {
|
||||
return { kind: "libCall", fn: "process.isTTY", args: [fd], type: BOOL, loc };
|
||||
}
|
||||
if (stream === "stdin") return null; // no columns on a ReadStream — generic fences apply
|
||||
if (stream === "stdin") return null; // no geometry on a ReadStream — generic fences apply
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
const want = lowerer.withUndefinedArm(F64);
|
||||
// JS files skip the annotation fence: there is no annotation to fix —
|
||||
@@ -6947,13 +6947,13 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
|
||||
// (commander's `isTTY ? columns : undefined` help-width probes).
|
||||
if ((!declared || typeKey(declared) !== typeKey(want)) && !isJsSourceFile(expr.getSourceFile())) {
|
||||
lowerer.noLowering(
|
||||
`process.${stream}.columns as a plain number`,
|
||||
`process.${stream}.${member} as a plain number`,
|
||||
expr,
|
||||
"on a non-TTY stream Node's .columns is undefined — type the read to admit it: " +
|
||||
`(process.${stream} as typeof process.${stream} & { columns?: number }).columns`,
|
||||
`on a non-TTY stream Node's .${member} is undefined — type the read to admit it: ` +
|
||||
`(process.${stream} as typeof process.${stream} & { ${member}?: number }).${member}`,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "process.columns", args: [fd], type: want, loc };
|
||||
return { kind: "libCall", fn: member === "rows" ? "process.rows" : "process.columns", args: [fd], type: want, loc };
|
||||
}
|
||||
|
||||
/** `process.argv` / `process.platform` / `process.pid` property READS
|
||||
|
||||
@@ -17,6 +17,7 @@ import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsuppo
|
||||
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
|
||||
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall, tryLowerNumericIndexRead } from "./lower-containers.js";
|
||||
import { arrayValueRead, arrayValueStore } from "./array-values.js";
|
||||
import { tryLowerIndexedComparison } from "./indexed-comparison.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
|
||||
import { fenceEnumObjectValue, lowerEnumAccess } from "./lower-enums.js";
|
||||
@@ -6740,6 +6741,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
||||
case ts.SyntaxKind.ExclamationEqualsEqualsToken: {
|
||||
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
const indexed = tryLowerIndexedComparison(lowerer, left, right, negated, loc);
|
||||
if (indexed) return indexed;
|
||||
if (plainBothNum) return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
if (bothStr) return { kind: "strEq", negated, left, right, type: BOOL, loc };
|
||||
// bool === bool: a plain value compare (the config-drift checks'
|
||||
|
||||
@@ -1173,6 +1173,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
fns: ["process.columns"],
|
||||
note: "the columns read on the process stdio streams (terminal geometry)",
|
||||
},
|
||||
{
|
||||
id: "node-builtin.process.rows",
|
||||
kind: "node-builtin",
|
||||
name: "process.rows",
|
||||
fns: ["process.rows"],
|
||||
note: "the rows read on process.stdout/stderr (terminal geometry)",
|
||||
},
|
||||
{
|
||||
id: "node-builtin.process.kill",
|
||||
kind: "node-builtin",
|
||||
|
||||
@@ -1577,6 +1577,10 @@ export type IrArrIntrinsicMethod =
|
||||
* returning the stored number or NaN for a hole/undefined/missing key.
|
||||
* Borrows the receiver and never traps on missing values. */
|
||||
| "getNumber"
|
||||
/** Internal strict equality of two f64/bool/string array slots. Arguments
|
||||
* are [left index, right array, right index]; holes and present undefined
|
||||
* compare as undefined. Both arrays have the same primitive element type. */
|
||||
| "indexEq"
|
||||
| "push"
|
||||
| "pushSpread"
|
||||
| "concatSpread"
|
||||
@@ -4428,6 +4432,8 @@ export type IrLibFn =
|
||||
* yields the undefined arm, exactly Node's missing `.columns`. Never
|
||||
* throws. */
|
||||
| "process.columns"
|
||||
/** Terminal height, with the same fd and optional-number contract. */
|
||||
| "process.rows"
|
||||
/** fromCharCode takes one packed f64[] or bytes arg and builds a string
|
||||
* from UTF-16 code units. Adjacent surrogate pairs combine; lone
|
||||
* surrogates follow the runtime's replacement policy. */
|
||||
@@ -7333,6 +7339,7 @@ export const LIB_NONDETERMINISTIC_PREFIXES: readonly [string, string][] = [
|
||||
["process.threadCpu", "the thread CPU clock (process.threadCpuUsage)"],
|
||||
["process.isTTY", "terminal attachment (isTTY)"],
|
||||
["process.columns", "terminal geometry (columns)"],
|
||||
["process.rows", "terminal geometry (rows)"],
|
||||
["process.kill", "process authority (kill)"],
|
||||
["process.umask", "process authority (umask)"],
|
||||
["process.exit", "process authority (exit)"],
|
||||
|
||||
@@ -24,6 +24,24 @@ test("numeric array-read intrinsic validates and round-trips", () => {
|
||||
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
|
||||
});
|
||||
|
||||
test("indexed equality validates primitive kinds, arguments, and result", () => {
|
||||
const args: IrExpr[] = [
|
||||
{ kind: "numLit", value: 0, type: F64, loc },
|
||||
{ kind: "arrayLit", elems: [], type: arrayOf(F64), loc },
|
||||
{ kind: "numLit", value: 1, type: F64, loc },
|
||||
];
|
||||
const mod = numericReadModule({ method: "indexEq", args, type: BOOL });
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
|
||||
for (const override of [
|
||||
{ args: [] }, { type: F64 },
|
||||
{ args: [args[0]!, { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }, args[2]!] },
|
||||
{ receiver: { kind: "arrayLit", elems: [], type: arrayOf(arrayOf(F64)), loc } },
|
||||
] satisfies Partial<IrExpr & { kind: "arrIntrinsic" }>[]) {
|
||||
expect(validateModule(numericReadModule({ method: "indexEq", args, type: BOOL, ...override }))).not.toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
[{ receiver: { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc } }, "requires f64 elements"],
|
||||
[{ args: [] }, "0 args, expected 1"],
|
||||
|
||||
@@ -1350,6 +1350,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
// Like process.envGet: the result is the module's interned
|
||||
// `number | undefined` union — checked by arms in the libCall case.
|
||||
"process.columns": { argTypes: [F64], result: VOID },
|
||||
"process.rows": { argTypes: [F64], result: VOID },
|
||||
"process.stdinDestroy": { argTypes: [], result: VOID },
|
||||
"process.stdinSetRawMode": { argTypes: [BOOL], result: VOID },
|
||||
// Arg 0 is a packed f64[] OR a bytes value (the spread-typed-array
|
||||
@@ -2776,6 +2777,8 @@ function validateFunction(
|
||||
? { argTypes: [e.receiver.type], result: F64 }
|
||||
: e.method === "nextPresent" || e.method === "getNumber"
|
||||
? { argTypes: [F64], result: F64 }
|
||||
: e.method === "indexEq"
|
||||
? { argTypes: [F64, e.receiver.type, F64], result: BOOL }
|
||||
: e.method === "pop"
|
||||
? { argTypes: [], result: e.type } // union-checked below
|
||||
: e.method === "indexOf"
|
||||
@@ -2808,6 +2811,9 @@ function validateFunction(
|
||||
if (e.method === "getNumber" && elem.kind !== "f64") {
|
||||
err(`arrIntrinsic getNumber requires f64 elements, got ${elem.kind}`, e.loc);
|
||||
}
|
||||
if (e.method === "indexEq" && elem.kind !== "f64" && elem.kind !== "bool" && elem.kind !== "string") {
|
||||
err(`arrIntrinsic indexEq requires primitive elements, got ${elem.kind}`, e.loc);
|
||||
}
|
||||
if (e.method === "flatCopy" && !typeEquals(e.type, e.receiver.type)) {
|
||||
err("arrIntrinsic flatCopy result must match its receiver", e.loc);
|
||||
}
|
||||
@@ -4038,7 +4044,7 @@ function validateFunction(
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e.fn === "process.columns") {
|
||||
if (e.fn === "process.columns" || e.fn === "process.rows") {
|
||||
// Result is the module's interned `number | undefined` union.
|
||||
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
|
||||
const ok =
|
||||
@@ -4047,7 +4053,7 @@ function validateFunction(
|
||||
def.arms[0]!.kind === "f64" &&
|
||||
def.arms[1]!.kind === "undefinedT";
|
||||
if (!ok) {
|
||||
err(`libCall process.columns must return the 'number | undefined' union`, e.loc);
|
||||
err(`libCall ${e.fn} must return the 'number | undefined' union`, e.loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1621,6 +1621,13 @@
|
||||
"status": "static",
|
||||
"note": "getrusage's 16 fields — every field read samples live machine state"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.process.rows",
|
||||
"kind": "node-builtin",
|
||||
"name": "process.rows",
|
||||
"status": "static",
|
||||
"note": "the rows read on process.stdout/stderr (terminal geometry)"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.process.threadCpuUsage",
|
||||
"kind": "node-builtin",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { emitCModule } from "../src/backend/c/c-emitter.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
import { F64, STRING, VOID, arrayOf, type IrExpr, type IrModule } from "../src/ir/ir.js";
|
||||
import { validateModule } from "../src/ir/validate.js";
|
||||
|
||||
test("scalar fromCharCode avoids a heap argument pack in both backends", () => {
|
||||
const loc = { file: "scalar-char.ts", start: 0, end: 0 };
|
||||
const value: IrExpr = { kind: "numLit", value: 65, type: F64, loc };
|
||||
const mod: IrModule = {
|
||||
irVersion: 11, sourceFile: loc.file, entry: "__main", globals: [],
|
||||
functions: [{
|
||||
name: "__main", params: [], returnType: VOID, locals: [], loc,
|
||||
body: [{ kind: "exprStmt", loc, expr: {
|
||||
kind: "libCall", fn: "string.fromCharCode", type: STRING, loc,
|
||||
args: [{ kind: "arrayLit", elems: [value], type: arrayOf(F64), loc }],
|
||||
} }],
|
||||
}],
|
||||
};
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const c = emitCModule(mod);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
expect(c).toContain("scr_str_from_char_code_one(");
|
||||
expect(llvm).toContain("call ptr @scr_str_from_char_code_one(double");
|
||||
expect(c).not.toContain("scr_arr_new(");
|
||||
expect(llvm).not.toContain("call ptr @scr_arr_new(");
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import { emitCModule } from "../src/backend/c/c-emitter.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
|
||||
test("primitive indexed comparisons do not allocate optional-union boxes", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-indexed-comparison-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, [
|
||||
"function strings(a: string[], b: string[], i: number): boolean { return a[i] === b[i]; }",
|
||||
"function numbers(a: number[], b: number[], i: number): boolean { return a[i] !== b[i]; }",
|
||||
"function bools(a: boolean[], b: boolean[], i: number): boolean { return a[i] === b[i]; }",
|
||||
"console.log(strings(['a'], ['a'], 0), numbers([1], [2], 0), bools([false], [false], 0));",
|
||||
].join("\n"));
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const visited = new Set<string>();
|
||||
function visit(value: unknown): void {
|
||||
if (!value || typeof value !== "object") return;
|
||||
if (Array.isArray(value)) { value.forEach(visit); return; }
|
||||
const node = value as { kind?: string; callee?: string };
|
||||
expect(node.kind).not.toBe("unionWrap");
|
||||
expect(node.kind).not.toBe("unionEq");
|
||||
if (node.kind === "call" && node.callee) visitFunction(node.callee);
|
||||
Object.values(value).forEach(visit);
|
||||
}
|
||||
function visitFunction(name: string): void {
|
||||
if (visited.has(name)) return;
|
||||
visited.add(name);
|
||||
const fn = mod.functions.find((f) => f.name === name);
|
||||
expect(fn).toBeDefined();
|
||||
visit(fn!.body);
|
||||
}
|
||||
visitFunction(mod.entry);
|
||||
const reachable = { ...mod, functions: mod.functions.filter((f) => visited.has(f.name)) };
|
||||
const c = emitCModule(reachable), llvm = emitLlvmModule(reachable);
|
||||
expect(c).not.toContain("scr_union_new_");
|
||||
expect(llvm).not.toContain("@scr_union_new_");
|
||||
expect(c.match(/scr_arr_index_eq\(/g)).toHaveLength(3);
|
||||
expect(llvm.match(/call zeroext i1 @scr_arr_index_eq\(/g)).toHaveLength(3);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -280,6 +280,16 @@ static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out,
|
||||
}
|
||||
|
||||
static void scr_arr_replace_owned(ScrArr *a, size_t index, uint64_t slot) {
|
||||
if (index < a->cap) {
|
||||
uint8_t state = a->present[index];
|
||||
uint64_t old = state == SCR_ARR_VALUE ? a->data[index] : 0;
|
||||
/* Publish the new edge before releasing the old one: a release can
|
||||
* collect cycles. Holes have uninitialized data and must not be read. */
|
||||
a->data[index] = slot;
|
||||
a->present[index] = SCR_ARR_VALUE;
|
||||
if (state == SCR_ARR_VALUE && scr_elem_is_ref(a->elem)) scr_elem_release(a, old);
|
||||
return;
|
||||
}
|
||||
uint64_t old;
|
||||
uint8_t state;
|
||||
bool had = scr_arr_take_state(a, index, &old, &state);
|
||||
@@ -766,7 +776,21 @@ double scr_arr_get_f64(ScrArr *a, double i) {
|
||||
return scr_slot_to_f64(scr_arr_require_slot(a, i));
|
||||
}
|
||||
|
||||
static bool scr_arr_read_value(const ScrArr *a, double i, uint64_t *slot) {
|
||||
size_t idx;
|
||||
uint8_t state;
|
||||
if (scr_arr_valid_index(i, &idx)) {
|
||||
if (idx >= a->len) return false;
|
||||
state = scr_arr_state_at(a, idx, slot);
|
||||
} else if (!scr_arr_prop_get_state(a, i, slot, &state)) {
|
||||
return false;
|
||||
}
|
||||
return state == SCR_ARR_VALUE;
|
||||
}
|
||||
|
||||
double scr_arr_get_number(const ScrArr *a, double i) {
|
||||
/* Keep this hot numeric-only entry self-contained: routing it through
|
||||
* scr_arr_read_value adds an out-of-line call at -O2 on native targets. */
|
||||
size_t idx;
|
||||
uint64_t slot;
|
||||
uint8_t state;
|
||||
@@ -779,6 +803,28 @@ double scr_arr_get_number(const ScrArr *a, double i) {
|
||||
return state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN;
|
||||
}
|
||||
|
||||
bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) {
|
||||
uint64_t av, bv;
|
||||
/* Avoid two general lookups for the common dense case. Check the range
|
||||
* before casting, and distinguish values from holes/present undefined. */
|
||||
if (i >= 0 && i < (double)a->cap && i == trunc(i) &&
|
||||
j >= 0 && j < (double)b->cap && j == trunc(j)) {
|
||||
size_t ai = (size_t)i, bi = (size_t)j;
|
||||
bool ap = ai < a->len && a->present[ai] == SCR_ARR_VALUE;
|
||||
bool bp = bi < b->len && b->present[bi] == SCR_ARR_VALUE;
|
||||
if (!ap || !bp) return ap == bp;
|
||||
av = a->data[ai];
|
||||
bv = b->data[bi];
|
||||
} else {
|
||||
bool ap = scr_arr_read_value(a, i, &av);
|
||||
bool bp = scr_arr_read_value(b, j, &bv);
|
||||
if (!ap || !bp) return ap == bp;
|
||||
}
|
||||
if (a->elem == SCR_ELEM_F64) return scr_slot_to_f64(av) == scr_slot_to_f64(bv);
|
||||
if (a->elem == SCR_ELEM_STR) return scr_str_eq(scr_slot_to_ptr(av), scr_slot_to_ptr(bv));
|
||||
return av == bv; /* bool; matching primitive kinds are validated by the IR */
|
||||
}
|
||||
|
||||
bool scr_arr_get_bool(ScrArr *a, double i) {
|
||||
return scr_arr_require_slot(a, i) != 0;
|
||||
}
|
||||
@@ -801,7 +847,6 @@ static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) {
|
||||
scr_arr_prop_set(a, i, slot);
|
||||
return;
|
||||
}
|
||||
idx = scr_arr_check_index(a, i, true);
|
||||
if (idx >= a->len) a->len = idx + 1;
|
||||
/* Unlink-then-release: a release can trigger a cycle collection, which
|
||||
* must never see a heap edge whose count was already given up. */
|
||||
@@ -826,7 +871,6 @@ void scr_arr_set_undefined(ScrArr *a, double i) {
|
||||
scr_arr_prop_set_undefined(a, i);
|
||||
return;
|
||||
}
|
||||
idx = scr_arr_check_index(a, i, true);
|
||||
if (idx >= a->len) a->len = idx + 1;
|
||||
scr_arr_replace_state_owned(a, idx, 0, SCR_ARR_UNDEFINED);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Model (see docs/ir.md):
|
||||
* - An async function's body is ordinary compiled C, run on its own fiber
|
||||
* (heap-allocated ucontext stack). Calling it runs the body EAGERLY until
|
||||
* (dedicated ucontext stack). Calling it runs the body EAGERLY until
|
||||
* the first suspension (JS's synchronous-prefix rule), then control
|
||||
* returns to the spawner with a +1 promise.
|
||||
* - `await` on a pending promise parks the fiber on the promise's waiter
|
||||
@@ -18,6 +18,13 @@
|
||||
* downgrades to a note in that case.
|
||||
*/
|
||||
#define _XOPEN_SOURCE 700
|
||||
/* Anonymous mappings alongside the XSI ucontext API on Darwin and glibc. */
|
||||
#ifndef _DARWIN_C_SOURCE
|
||||
#define _DARWIN_C_SOURCE 1
|
||||
#endif
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE 1
|
||||
#endif
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#include <errno.h>
|
||||
@@ -43,6 +50,7 @@
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@@ -80,14 +88,65 @@ void __sanitizer_finish_switch_fiber(void *fake_stack_save, const void **bottom_
|
||||
* the measured need: an engine call costs 64–96KB there, and a real
|
||||
* embedded graph entered FROM A FIBER (a commander action awaiting
|
||||
* generateText — zod parses inside promise chains) nests dozens of engine
|
||||
* frames; the memory is malloc'd and committed lazily, so idle fibers pay
|
||||
* address space, not RSS. */
|
||||
* frames; native POSIX stacks are mapped and committed lazily, so idle
|
||||
* fibers pay address space, not RSS. */
|
||||
#ifdef SCR_ASAN_FIBERS
|
||||
#define SCR_FIBER_STACK (8 * 1024 * 1024)
|
||||
#else
|
||||
#define SCR_FIBER_STACK (256 * 1024)
|
||||
#endif
|
||||
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
/* Keep stacks out of malloc's size classes. Reuse a bounded number during
|
||||
* a burst of promise/generator work, then unmap the spares before the loop
|
||||
* sleeps. This avoids both heap fragmentation and per-call mmap overhead.
|
||||
* The sanitizer lane unmaps every finished stack immediately. */
|
||||
#ifndef SCR_ASAN_FIBERS
|
||||
#define SCR_FIBER_SPARES 4
|
||||
static SCR_TL void *scr_fiber_spares[SCR_FIBER_SPARES];
|
||||
static SCR_TL size_t scr_fiber_nspares;
|
||||
static SCR_TL bool scr_fiber_cleanup_registered;
|
||||
#endif
|
||||
|
||||
static void scr_fiber_stacks_clear(void) {
|
||||
#ifndef SCR_ASAN_FIBERS
|
||||
while (scr_fiber_nspares > 0) {
|
||||
(void)munmap(scr_fiber_spares[--scr_fiber_nspares], SCR_FIBER_STACK);
|
||||
scr_fiber_spares[scr_fiber_nspares] = NULL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void *scr_fiber_stack_new(void) {
|
||||
#ifndef SCR_ASAN_FIBERS
|
||||
if (scr_fiber_nspares > 0) {
|
||||
void *stack = scr_fiber_spares[--scr_fiber_nspares];
|
||||
scr_fiber_spares[scr_fiber_nspares] = NULL;
|
||||
return stack;
|
||||
}
|
||||
if (!scr_fiber_cleanup_registered) {
|
||||
scr_fiber_cleanup_registered = true;
|
||||
scr_atexit(scr_fiber_stacks_clear);
|
||||
}
|
||||
#endif
|
||||
void *stack = mmap(NULL, SCR_FIBER_STACK, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (stack == MAP_FAILED) scr_trap("scriptc: out of memory\n");
|
||||
return stack;
|
||||
}
|
||||
|
||||
static void scr_fiber_stack_free(void *stack) {
|
||||
if (!stack) return;
|
||||
#ifndef SCR_ASAN_FIBERS
|
||||
if (scr_fiber_nspares < SCR_FIBER_SPARES) {
|
||||
scr_fiber_spares[scr_fiber_nspares++] = stack;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
(void)munmap(stack, SCR_FIBER_STACK);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── promises ─────────────────────────────────────────────────────────── */
|
||||
|
||||
enum { SCR_PROM_PENDING = 0, SCR_PROM_FULFILLED = 1, SCR_PROM_REJECTED = 2 };
|
||||
@@ -1209,7 +1268,11 @@ static void scr_fiber_destroy(ScrFiber *f) {
|
||||
if (f->coro != NULL) scr_wasi_coro_destroy(f->coro);
|
||||
#endif
|
||||
scr_als_ctx_release(f->als);
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
scr_fiber_stack_free(f->stack);
|
||||
#else
|
||||
free(f->stack);
|
||||
#endif
|
||||
free(f);
|
||||
}
|
||||
|
||||
@@ -1249,8 +1312,7 @@ ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) {
|
||||
}
|
||||
return result;
|
||||
#else
|
||||
f->stack = malloc(SCR_FIBER_STACK);
|
||||
if (!f->stack) scr_oom();
|
||||
f->stack = scr_fiber_stack_new();
|
||||
getcontext(&f->ctx);
|
||||
f->ctx.uc_stack.ss_sp = f->stack;
|
||||
f->ctx.uc_stack.ss_size = SCR_FIBER_STACK;
|
||||
@@ -2620,6 +2682,9 @@ bool scr_loop_run(ScrPromise *top_level) {
|
||||
* during that turn. No fd will wake us for this userspace work: return to
|
||||
* dispatch without sleeping, still allowing due timers and immediates. */
|
||||
if (scr_children_ready()) due = now;
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
if (due > now) scr_fiber_stacks_clear();
|
||||
#endif
|
||||
bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn();
|
||||
if (io) {
|
||||
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
||||
@@ -3208,8 +3273,7 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa
|
||||
#elif defined(__wasi__)
|
||||
f->ctx = 0;
|
||||
#else
|
||||
f->stack = malloc(SCR_FIBER_STACK);
|
||||
if (!f->stack) scr_oom();
|
||||
f->stack = scr_fiber_stack_new();
|
||||
getcontext(&f->ctx);
|
||||
f->ctx.uc_stack.ss_sp = f->stack;
|
||||
f->ctx.uc_stack.ss_size = SCR_FIBER_STACK;
|
||||
|
||||
@@ -3207,28 +3207,33 @@ ScrStr *scr_atomics_wait(ScrBytes *arr, double idx, double expected, double time
|
||||
|
||||
bool scr_process_is_tty(double fd) { return isatty((int)fd) != 0; }
|
||||
|
||||
/* Terminal width for process.stdout/stderr.columns: ioctl(TIOCGWINSZ) on
|
||||
/* Terminal geometry for process.stdout/stderr.columns/rows: ioctl(TIOCGWINSZ) on
|
||||
* the stream's fd, exactly Node's tty.WriteStream source of truth. A
|
||||
* non-TTY stream, or a terminal that refuses the ioctl, answers -1 and
|
||||
* the emitter's union construction turns that into the undefined arm —
|
||||
* Node's missing `.columns` on non-TTY streams. */
|
||||
double scr_process_columns(double fd) {
|
||||
* Node's missing geometry on non-TTY streams. */
|
||||
static double scr_process_dimension(double fd, bool rows) {
|
||||
if (!isatty((int)fd)) return -1;
|
||||
#ifdef _WIN32
|
||||
/* The console buffer's window width — libuv's uv_tty_get_winsize. */
|
||||
/* The console buffer's visible window — libuv's uv_tty_get_winsize. */
|
||||
HANDLE h = (HANDLE)_get_osfhandle((int)fd);
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
if (h == INVALID_HANDLE_VALUE || !GetConsoleScreenBufferInfo(h, &info)) return -1;
|
||||
return (double)(info.srWindow.Right - info.srWindow.Left + 1);
|
||||
return rows ? (double)(info.srWindow.Bottom - info.srWindow.Top + 1)
|
||||
: (double)(info.srWindow.Right - info.srWindow.Left + 1);
|
||||
#elif defined(__wasi__)
|
||||
(void)rows;
|
||||
return -1;
|
||||
#else
|
||||
struct winsize ws;
|
||||
if (ioctl((int)fd, TIOCGWINSZ, &ws) != 0) return -1;
|
||||
return (double)ws.ws_col;
|
||||
return rows ? (double)ws.ws_row : (double)ws.ws_col;
|
||||
#endif
|
||||
}
|
||||
|
||||
double scr_process_columns(double fd) { return scr_process_dimension(fd, false); }
|
||||
double scr_process_rows(double fd) { return scr_process_dimension(fd, true); }
|
||||
|
||||
/* process.stdin.setRawMode(mode). TTY stdin: libuv's UV_TTY_MODE_RAW
|
||||
* termios flag set — exactly what Node's setRawMode(true) applies — and
|
||||
* setRawMode(false) restores the termios saved at the first raw entry
|
||||
@@ -5052,6 +5057,7 @@ ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem) {
|
||||
* Node writing a lone surrogate to stdout produces the same replacement
|
||||
* bytes), UTF-8 encode. */
|
||||
static ScrStr *scr_str_from_units(size_t n, uint32_t (*unit)(void *, size_t), void *src) {
|
||||
if (n == 1) return scr_str_from_char_code_one((double)unit(src, 0));
|
||||
char *out = malloc(n * 3 + 1); /* worst case: 3 bytes per UTF-16 unit */
|
||||
if (!out) {
|
||||
scr_trap("scriptc: out of memory\n");
|
||||
|
||||
@@ -1020,6 +1020,9 @@ double scr_math_random(void);
|
||||
double scr_arr_get_f64(ScrArr *a, double i); /* trap missing/hole */
|
||||
/* ToNumber(a[i]) for f64 storage: borrows a; missing/hole/undefined -> NaN. */
|
||||
double scr_arr_get_number(const ScrArr *a, double i);
|
||||
/* Strict equality of ordinary indexed reads on matching primitive arrays.
|
||||
* Borrows both arrays, preserves undefined vs NaN, and never allocates. */
|
||||
bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j);
|
||||
bool scr_arr_get_bool(ScrArr *a, double i); /* trap missing/hole */
|
||||
void *scr_arr_get_ref(ScrArr *a, double i); /* trap missing/hole; +1 */
|
||||
|
||||
@@ -2416,6 +2419,8 @@ void scr_process_stdin_set_raw_mode(bool raw);
|
||||
* union's number arm and -1 into its undefined arm (Node's non-TTY
|
||||
* `.columns` is undefined). Never throws. */
|
||||
double scr_process_columns(double fd);
|
||||
/* Terminal height; same optional-number sentinel contract as columns. */
|
||||
double scr_process_rows(double fd);
|
||||
|
||||
/* Stats values (statSync/lstatSync and their fs.promises twins): an immutable
|
||||
* snapshot of stat(2). scr_fs_stat follows symlinks; scr_fs_lstat does not.
|
||||
@@ -5014,6 +5019,9 @@ ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */
|
||||
* (divergence 1's policy). Borrowed args; the string result is +1; neither
|
||||
* throws. */
|
||||
ScrStr *scr_str_from_char_code(ScrArr *codes);
|
||||
/* Single numeric argument; avoids argument packing. Defined in scr_string.c
|
||||
* to share its character cache. Same ToUint16 and lone-surrogate behavior. */
|
||||
ScrStr *scr_str_from_char_code_one(double code);
|
||||
/* The spread-typed-array form (String.fromCharCode(...bytes) — the
|
||||
* magic-number ASCII probe); same semantics per element. */
|
||||
ScrStr *scr_str_from_char_code_bytes(ScrBytes *codes);
|
||||
|
||||
@@ -18,6 +18,26 @@ static void scr_oom(void) {
|
||||
scr_trap("scriptc: out of memory\n");
|
||||
}
|
||||
|
||||
/* Weak, bounded interning for tiny slices (including non-ASCII characters).
|
||||
* Unlike an owning cache, this never keeps a string alive: its last release
|
||||
* removes the entry. Collisions simply replace the weak pointer. A unique
|
||||
* string can still be appended to or reallocated, so those paths invalidate
|
||||
* the entry before touching its bytes/address. No ScrStr ABI change. */
|
||||
#define SCR_SHORT_N 64
|
||||
static SCR_TL ScrStr *scr_short_tab[SCR_SHORT_N];
|
||||
|
||||
static size_t scr_short_hash(const char *bytes, size_t len) {
|
||||
size_t h = len;
|
||||
for (size_t i = 0; i < len; i++) h = h * 31 + (unsigned char)bytes[i];
|
||||
return h % SCR_SHORT_N;
|
||||
}
|
||||
|
||||
static void scr_short_forget(const ScrStr *s) {
|
||||
if (s->len < 2 || s->len > 4) return;
|
||||
size_t h = scr_short_hash(s->data, s->len);
|
||||
if (scr_short_tab[h] == s) scr_short_tab[h] = NULL;
|
||||
}
|
||||
|
||||
/* ── UTF-16 index cache ───────────────────────────────────────────────
|
||||
* JS string semantics are UTF-16 indices over our UTF-8 storage, so
|
||||
* .length, charCodeAt, charAt, indexOf and slice all need unit↔byte
|
||||
@@ -251,6 +271,7 @@ ScrStr *scr_str_alloc_raw(size_t len, size_t cap) {
|
||||
}
|
||||
|
||||
ScrStr *scr_str_regrow(ScrStr *s, size_t newcap) {
|
||||
scr_short_forget(s);
|
||||
scr_sidx_purge(s); /* realloc may move; the old address may be recycled */
|
||||
ScrStr *r = realloc(s, sizeof(ScrStr) + newcap + 1);
|
||||
if (!r) scr_oom();
|
||||
@@ -261,6 +282,7 @@ ScrStr *scr_str_regrow(ScrStr *s, size_t newcap) {
|
||||
void scr_str_release(ScrStr *s) {
|
||||
if (!s || s->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
|
||||
if (--s->rc == 0) {
|
||||
scr_short_forget(s);
|
||||
scr_sidx_purge(s); /* the address may be recycled by the next malloc */
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_strings--;
|
||||
@@ -287,6 +309,7 @@ ScrStr *scr_str_concat(ScrStr *a, ScrStr *b) {
|
||||
* with rc > 1 might be aliased and is copied, never mutated. */
|
||||
if (a->rc == 1 && a != b && a->cap >= newlen) {
|
||||
size_t oldlen = a->len;
|
||||
scr_short_forget(a);
|
||||
memcpy(a->data + a->len, b->data, b->len);
|
||||
a->len = newlen;
|
||||
a->data[newlen] = '\0';
|
||||
@@ -410,15 +433,43 @@ static const struct { size_t rc; size_t len; size_t cap; char data[1]; }
|
||||
|
||||
static ScrStr *scr_str_empty(void) { return (ScrStr *)&scr_lit_empty; }
|
||||
|
||||
/* Interned when the content is empty or one ASCII byte; fresh otherwise. */
|
||||
/* Empty/ASCII characters are immortal; tiny spans share live heap strings. */
|
||||
static ScrStr *scr_str_from_span(const char *bytes, size_t len) {
|
||||
if (len == 0) return scr_str_empty();
|
||||
if (len == 1 && (unsigned char)bytes[0] < 0x80) {
|
||||
return (ScrStr *)&scr_ascii1[(unsigned char)bytes[0]];
|
||||
}
|
||||
if (len >= 2 && len <= 4) {
|
||||
size_t h = scr_short_hash(bytes, len);
|
||||
ScrStr *cached = scr_short_tab[h];
|
||||
if (cached && cached->len == len && memcmp(cached->data, bytes, len) == 0)
|
||||
return scr_str_retain(cached);
|
||||
ScrStr *s = scr_str_new(bytes, len);
|
||||
scr_short_tab[h] = s;
|
||||
return s;
|
||||
}
|
||||
return scr_str_new(bytes, len);
|
||||
}
|
||||
|
||||
/* A scalar fromCharCode needs neither the variadic argument array nor an
|
||||
* encoding buffer. Reuse the same empty/ASCII/tiny-span storage policy as
|
||||
* character indexing. A lone surrogate keeps the existing U+FFFD policy. */
|
||||
ScrStr *scr_str_from_char_code_one(double code) {
|
||||
uint32_t cp = scr_to_uint32(code) & 0xFFFFu;
|
||||
if (cp < 0x80) return (ScrStr *)&scr_ascii1[cp];
|
||||
if (cp >= 0xD800 && cp <= 0xDFFF) cp = 0xFFFD;
|
||||
char bytes[3];
|
||||
if (cp < 0x800) {
|
||||
bytes[0] = (char)(0xC0 | (cp >> 6));
|
||||
bytes[1] = (char)(0x80 | (cp & 0x3F));
|
||||
return scr_str_from_span(bytes, 2);
|
||||
}
|
||||
bytes[0] = (char)(0xE0 | (cp >> 12));
|
||||
bytes[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
bytes[2] = (char)(0x80 | (cp & 0x3F));
|
||||
return scr_str_from_span(bytes, 3);
|
||||
}
|
||||
|
||||
/* ── string methods: UTF-16 semantics over UTF-8 storage ──────────
|
||||
* All strings in the system are well-formed UTF-8 (the compiler replaces
|
||||
* lone surrogates in literals with U+FFFD), so the decode helpers below
|
||||
|
||||
@@ -108,6 +108,37 @@ static void test_bool(void) {
|
||||
scr_arr_release(a);
|
||||
}
|
||||
|
||||
static void test_dense_replacement(void) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
long strings0 = scr_str_live_count();
|
||||
#endif
|
||||
ScrArr *a = scr_arr_new(SCR_ELEM_STR, 32);
|
||||
scr_arr_set_ref(a, 7, scr_str_new("first", 5));
|
||||
check(scr_arr_len(a) == 8 && !scr_arr_has(a, 6),
|
||||
"write within capacity grows length without filling holes");
|
||||
/* Self-assignment moves the getter's retained reference back in. */
|
||||
scr_arr_set_ref(a, 7, scr_arr_get_ref(a, 7));
|
||||
ScrStr *s = scr_arr_get_ref(a, 7);
|
||||
check(s->rc == 2 && strcmp(s->data, "first") == 0,
|
||||
"self replacement preserves exactly one stored reference");
|
||||
scr_str_release(s);
|
||||
scr_arr_set_undefined(a, 7);
|
||||
scr_arr_set_ref(a, 7, scr_str_new("second", 6));
|
||||
scr_arr_delete(a, 7);
|
||||
scr_arr_set_ref(a, 7, scr_str_new("third", 5));
|
||||
scr_arr_set_len(a, 0);
|
||||
scr_arr_set_ref(a, 9, scr_str_new("last", 4));
|
||||
check(!scr_arr_has(a, 7) && scr_arr_len(a) == 10,
|
||||
"refill after truncation does not resurrect old entries");
|
||||
s = scr_arr_get_ref(a, 9);
|
||||
check(strcmp(s->data, "last") == 0, "refill after truncation owns new value");
|
||||
scr_str_release(s);
|
||||
scr_arr_release(a);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
check(scr_str_live_count() == strings0, "dense replacements release every string");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_unshift_reverse(void) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
long strings0 = scr_str_live_count();
|
||||
@@ -626,6 +657,7 @@ int main(int argc, char **argv) {
|
||||
test_f64_basics();
|
||||
test_numeric_read();
|
||||
test_bool();
|
||||
test_dense_replacement();
|
||||
test_unshift_reverse();
|
||||
test_str_rc();
|
||||
test_nested_rc();
|
||||
|
||||
@@ -266,6 +266,78 @@ static void accumulation_asserts(void) {
|
||||
scr_str_release(large);
|
||||
}
|
||||
|
||||
static void short_string_asserts(void) {
|
||||
// Scalar construction shares ASCII without allocating, including NUL.
|
||||
ScrStr *ascii = scr_str_from_char_code_one(65.9);
|
||||
ScrStr *wrapped = scr_str_from_char_code_one(65536 + 65);
|
||||
ScrStr *nul = scr_str_from_char_code_one(INFINITY);
|
||||
if (ascii != wrapped || ascii->rc != SIZE_MAX || ascii->len != 1 ||
|
||||
ascii->data[0] != 'A' || nul->len != 1 || nul->data[0] != 0) {
|
||||
failed++;
|
||||
fprintf(stderr, "SCALAR CHAR: ASCII cache or ToUint16 mismatch\n");
|
||||
}
|
||||
scr_str_release(ascii);
|
||||
scr_str_release(wrapped);
|
||||
scr_str_release(nul);
|
||||
|
||||
ScrStr *unit = scr_str_from_char_code_one(0x2500);
|
||||
ScrStr *same = scr_str_from_char_code_one(0x2500);
|
||||
if (unit != same || unit->rc != 2 || unit->len != 3 ||
|
||||
memcmp(unit->data, "\xE2\x94\x80", 3) != 0) {
|
||||
failed++;
|
||||
fprintf(stderr, "SCALAR CHAR: Unicode sharing mismatch\n");
|
||||
}
|
||||
scr_str_release(unit);
|
||||
scr_str_release(same);
|
||||
|
||||
ScrStr *source = scr_str_new("\xE2\x94\x80\xE2\x94\x80", 6);
|
||||
ScrStr *a = scr_str_char_at(source, 0);
|
||||
ScrStr *b = scr_str_char_at(source, 1);
|
||||
if (a != b || a->rc != 2) {
|
||||
failed++;
|
||||
fprintf(stderr, "SHORT: repeated Unicode character was not shared\n");
|
||||
}
|
||||
scr_str_release(a);
|
||||
scr_str_release(b);
|
||||
// Lookup after the last owner dies must never read a stale weak entry.
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ScrStr *c = scr_str_char_at(source, 0);
|
||||
if (c->len != 3 || memcmp(c->data, source->data, 3) != 0) failed++;
|
||||
scr_str_release(c);
|
||||
}
|
||||
// More live values than cache slots force collisions without invalidating
|
||||
// old owners. Embedded NULs are content, not terminators for the key.
|
||||
ScrStr *held[256];
|
||||
for (int i = 0; i < 256; i++) {
|
||||
char bytes[3] = {(char)('a' + i / 16), 0, (char)('a' + i % 16)};
|
||||
ScrStr *input = scr_str_new(bytes, 3);
|
||||
held[i] = scr_str_slice(input, 0, 3);
|
||||
scr_str_release(input);
|
||||
}
|
||||
for (int i = 0; i < 256; i++) {
|
||||
char bytes[3] = {(char)('a' + i / 16), 0, (char)('a' + i % 16)};
|
||||
if (held[i]->len != 3 || memcmp(held[i]->data, bytes, 3) != 0) failed++;
|
||||
scr_str_release(held[i]);
|
||||
}
|
||||
// Regrow may move the allocation. Append must also forget a cached key
|
||||
// before a uniquely owned string's bytes change.
|
||||
a = scr_str_char_at(source, 0);
|
||||
a = scr_str_regrow(a, 32);
|
||||
ScrStr *suffix = scr_str_new("x", 1);
|
||||
handoff_append(&a, suffix);
|
||||
b = scr_str_char_at(source, 0);
|
||||
if (a->len != 4 || b->len != 3 || memcmp(b->data, source->data, 3) != 0) failed++;
|
||||
// Sharing must preserve an observable alias across concat.
|
||||
ScrStr *alias = scr_str_char_at(source, 1);
|
||||
handoff_append(&b, suffix);
|
||||
if (alias->len != 3 || b->len != 4) failed++;
|
||||
scr_str_release(alias);
|
||||
scr_str_release(b);
|
||||
scr_str_release(a);
|
||||
scr_str_release(suffix);
|
||||
scr_str_release(source);
|
||||
}
|
||||
|
||||
#ifdef SCR_SIDX_TEST
|
||||
static void sidx_fail(const char *what) {
|
||||
failed++;
|
||||
@@ -662,6 +734,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
divergence_asserts();
|
||||
accumulation_asserts();
|
||||
short_string_asserts();
|
||||
#ifdef SCR_SIDX_TEST
|
||||
sparse_index_asserts();
|
||||
sparse_ascii_prefix_asserts();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Ordinary primitive reads compare without boxes, but preserve JS absence,
|
||||
// NaN, signed zero, numeric properties, evaluation order and owned snapshots.
|
||||
function numbers(a: number[], b: number[], i: number, j: number): void {
|
||||
console.log(a[i] === b[j], a[i] !== b[j]);
|
||||
}
|
||||
function strings(a: string[], b: string[], i: number, j: number): void {
|
||||
console.log(a[i] === b[j], a[i] !== b[j]);
|
||||
}
|
||||
function booleans(a: boolean[], b: boolean[], i: number, j: number): void {
|
||||
console.log(a[i] === b[j], a[i] !== b[j]);
|
||||
}
|
||||
const a = [0, -0, NaN, Infinity, 3];
|
||||
const b = [0, -0, NaN, Infinity, 4];
|
||||
const indices = [0, 1, 2, 3, 4, 8, -1, 1.5, NaN, Infinity, 4294967295];
|
||||
a[-1] = 7; b[-1] = 7;
|
||||
a[1.5] = 9; b[1.5] = 10;
|
||||
a[NaN] = 11; b[NaN] = 11;
|
||||
for (const i of indices) for (const j of indices) numbers(a, b, i, j);
|
||||
const empty: number[] = [];
|
||||
const sparse: number[] = [];
|
||||
sparse[1] = empty[0];
|
||||
sparse[2] = 0;
|
||||
sparse[3] = NaN;
|
||||
for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) numbers(sparse, sparse, i, j);
|
||||
for (let i = -1; i < 5; i++) for (let j = -1; j < 5; j++) {
|
||||
strings(["", "é", "┌", "same"], ["", "e", "┌", "same"], i, j);
|
||||
booleans([false, true], [false, true], i, j);
|
||||
}
|
||||
let trace = "";
|
||||
const text = ["left".repeat(20)];
|
||||
function receiver(label: string): string[] { trace += label; return text; }
|
||||
function index(label: string): number { trace += label; return 0; }
|
||||
function replace(): string[] { trace += "R"; text[0] = "right".repeat(20); return text; }
|
||||
console.log(receiver("L")[index("i")] === replace()[index("j")], trace);
|
||||
console.log(text[0]);
|
||||
function fail(): number { trace += "!"; throw new Error("index"); }
|
||||
try { console.log(receiver("A")[index("a")] === receiver("B")[fail()]); }
|
||||
catch (error) { console.log(error instanceof Error, trace); }
|
||||
// Repeated conditions exercise hidden-local cleanup on continue/break and
|
||||
// exceptions; both backends run under the RC audit as well as plain mode.
|
||||
let count = 0;
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const x = ["x".repeat(i % 17 + 1)];
|
||||
const y = ["x".repeat(i % 17 + 1)];
|
||||
if (x[0] !== y[0]) break;
|
||||
if (x[1] === y[1]) { count++; continue; }
|
||||
}
|
||||
console.log("count", count);
|
||||
let n = 0;
|
||||
while (["a".repeat(++n)][0] === ["a".repeat(n)][0]) if (n === 40) break;
|
||||
console.log("condition", n);
|
||||
@@ -0,0 +1,5 @@
|
||||
// Redirected stdio has no geometry. PTY reads and resize are covered by the
|
||||
// terminal-geometry harness; both backends must also preserve undefined here.
|
||||
const out = (process.stdout as typeof process.stdout & { rows?: number }).rows;
|
||||
const err = (process.stderr as typeof process.stderr & { rows?: number }).rows;
|
||||
console.log(out === undefined, err === undefined, out ?? 24, err ?? 48);
|
||||
@@ -0,0 +1,48 @@
|
||||
// Stack-local values survive nested suspension, rejection, generator return,
|
||||
// and repeated creation/destruction of native execution stacks.
|
||||
async function nested(depth: number, seed: number): Promise<string> {
|
||||
const local = ["frame-" + depth, "seed-" + seed];
|
||||
await Promise.resolve(seed);
|
||||
if (depth === 0) return local.join(":");
|
||||
const inner = await nested(depth - 1, seed + 1);
|
||||
return local[0] + "/" + inner + "/" + local[1];
|
||||
}
|
||||
|
||||
function* frames(seed: number): Generator<string, string, number> {
|
||||
const local = ["start-" + seed, "finish-" + seed];
|
||||
try {
|
||||
const sent = yield local[0];
|
||||
yield local[1] + ":" + sent;
|
||||
return local.join("/");
|
||||
} finally {
|
||||
if (seed < 2) console.log("cleanup", local[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const simultaneous = [nested(8, 1), nested(6, 100)];
|
||||
console.log((await Promise.all(simultaneous)).join(" | "));
|
||||
let total = 0;
|
||||
for (let i = 0; i < 120; i++) {
|
||||
const g = frames(i);
|
||||
total += String(g.next(0).value).length;
|
||||
total += String(g.next(i + 7).value).length;
|
||||
total += String(g.return("returned-" + i).value).length;
|
||||
total += (await nested(2, i)).length;
|
||||
}
|
||||
console.log("total", total);
|
||||
const parked = frames(300);
|
||||
console.log(parked.next(0).value);
|
||||
// The event loop can release spare stacks while this generator stays live.
|
||||
await new Promise<void>((resolve) => { setTimeout(() => resolve(), 5); });
|
||||
console.log(parked.next(17).value, parked.next(0).value);
|
||||
const completed = frames(200);
|
||||
console.log(completed.next(0).value, completed.next(9).value,
|
||||
completed.next(0).value, completed.next(0).done);
|
||||
try {
|
||||
await Promise.reject(new Error(await nested(1, 10)));
|
||||
} catch (error) {
|
||||
console.log(error instanceof Error ? error.message : "wrong error");
|
||||
}
|
||||
}
|
||||
run();
|
||||
@@ -0,0 +1,30 @@
|
||||
// The scalar path keeps ToUint16 wrapping and evaluates its input once.
|
||||
let calls = 0;
|
||||
function next(): number { calls++; return 64 + calls; }
|
||||
console.log(String.fromCharCode(next()), calls);
|
||||
console.log(String.fromCharCode(...[next()]), calls);
|
||||
|
||||
const values = [0, -0, 1, 65.9, -65.9, 127, 128, 255, 2047, 2048,
|
||||
0x2500, 0xD7FF, 0xD800, 0xDBFF, 0xDC00, 0xDFFF, 0xE000, 0xFFFF,
|
||||
65536, 65537, -1, -65537, NaN, Infinity, -Infinity, 2 ** 40 + 65];
|
||||
for (const value of values) {
|
||||
const scalar = String.fromCharCode(value);
|
||||
const spread = String.fromCharCode(...[value]);
|
||||
const bytes = String.fromCharCode(...new Float64Array([value]));
|
||||
// Write lone surrogates as UTF-8 to exercise the runtime's documented
|
||||
// replacement policy without asserting unsupported internal surrogates.
|
||||
console.log(Buffer.from(scalar).toString("hex"), Buffer.from(spread).toString("hex"), Buffer.from(bytes).toString("hex"));
|
||||
}
|
||||
|
||||
function fail(): number { throw new Error("argument failed"); }
|
||||
try { console.log(String.fromCharCode(fail())); } catch (error) {
|
||||
console.log(error instanceof Error, calls);
|
||||
}
|
||||
console.log(String.fromCharCode(0xD83D, 0xDE00));
|
||||
|
||||
// Retaining earlier results must not expose cache mutation or stale pointers.
|
||||
const held: string[] = [];
|
||||
for (let i = 0; i < 192; i++) held.push(String.fromCharCode(0x2500 + i));
|
||||
let total = 0;
|
||||
for (let i = 0; i < held.length; i++) total += held[i]!.charCodeAt(0);
|
||||
console.log(total, held[0], held[191]);
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
function geometry(): number[] {
|
||||
return [
|
||||
(process.stdout as typeof process.stdout & { columns?: number }).columns ?? -1,
|
||||
(process.stdout as typeof process.stdout & { rows?: number }).rows ?? -1,
|
||||
(process.stderr as typeof process.stderr & { columns?: number }).columns ?? -1,
|
||||
(process.stderr as typeof process.stderr & { rows?: number }).rows ?? -1,
|
||||
];
|
||||
}
|
||||
console.log(JSON.stringify(geometry()));
|
||||
let ticks = 0;
|
||||
const timer = setInterval(() => {
|
||||
const size = geometry();
|
||||
if (size[0] === 100 && size[1] === 52 && size[2] === 80 && size[3] === 24) {
|
||||
console.log(JSON.stringify(size));
|
||||
clearInterval(timer);
|
||||
} else if (++ticks > 500) {
|
||||
clearInterval(timer);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}, 5);
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
"""Give stdout/stderr independent dimensions, then resize both live TTYs."""
|
||||
import fcntl, json, os, pty, select, signal, struct, subprocess, sys, termios, time
|
||||
|
||||
out_master, out_slave = pty.openpty()
|
||||
err_master, err_slave = pty.openpty()
|
||||
def resize(fd, columns, rows):
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', rows, columns, 0, 0))
|
||||
|
||||
resize(out_slave, 88, 48)
|
||||
resize(err_slave, 72, 30)
|
||||
proc = subprocess.Popen(sys.argv[1:], stdin=subprocess.DEVNULL, stdout=out_slave, stderr=err_slave)
|
||||
pending = b''
|
||||
errors = bytearray()
|
||||
lines = []
|
||||
deadline = time.monotonic() + 10
|
||||
try:
|
||||
while len(lines) < 2 and time.monotonic() < deadline:
|
||||
for fd in select.select([out_master, err_master], [], [], 0.05)[0]:
|
||||
data = os.read(fd, 65536)
|
||||
if fd == err_master:
|
||||
errors.extend(data)
|
||||
continue
|
||||
pending += data
|
||||
while b'\n' in pending:
|
||||
line, pending = pending.split(b'\n', 1)
|
||||
lines.append(json.loads(line))
|
||||
if len(lines) == 1:
|
||||
assert lines[0] == [88, 48, 72, 30], lines
|
||||
resize(out_slave, 100, 52)
|
||||
resize(err_slave, 80, 24)
|
||||
# Node refreshes its cached properties on SIGWINCH.
|
||||
os.kill(proc.pid, signal.SIGWINCH)
|
||||
proc.wait(timeout=3)
|
||||
while select.select([err_master], [], [], 0)[0]:
|
||||
errors.extend(os.read(err_master, 65536))
|
||||
assert proc.returncode == 0 and not errors, (proc.returncode, errors)
|
||||
assert lines == [[88, 48, 72, 30], [100, 52, 80, 24]], lines
|
||||
print(json.dumps(lines))
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
for fd in [out_master, out_slave, err_master, err_slave]: os.close(fd)
|
||||
@@ -0,0 +1,30 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const fixture = join(import.meta.dirname, "../fixtures/terminal-geometry");
|
||||
|
||||
test.skipIf(process.platform === "win32")("stdio geometry matches Node on independent resized PTYs", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-geometry-"));
|
||||
try {
|
||||
const entry = join(fixture, "main.ts");
|
||||
const script = join(fixture, "observe.py");
|
||||
const oracle = await run("python3", [script, process.execPath, "--experimental-strip-types", entry]);
|
||||
expect(JSON.parse(oracle.stdout)).toEqual([[88, 48, 72, 30], [100, 52, 80, 24]]);
|
||||
expect(oracle.stderr).toBe("");
|
||||
for (const backend of ["c", "llvm"] as const) {
|
||||
const result = await compile(entry, { outDir: dir, outPath: join(dir, backend), backend, sanitize: process.env["SCRIPTC_SAN"] === "1" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const native = await run("python3", [script, result.binaryPath]);
|
||||
expect(native.stdout).toBe(oracle.stdout);
|
||||
expect(native.stderr).toBe("");
|
||||
}
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}, 60_000);
|
||||
Reference in New Issue
Block a user