Support native callable properties and terminal streams

- Preserve callable properties, descriptors, and shared module bindings across native JavaScript code.
- Add stored process streams with input control, output callbacks, and write interception.
- Preserve Error identity and classification through native rethrows and promise rejection.
This commit is contained in:
Chris Tate
2026-09-29 00:52:01 -05:00
committed by GitHub
parent 97a489845a
commit a36b356a6c
37 changed files with 1207 additions and 152 deletions
+2
View File
@@ -105,6 +105,8 @@ A static-tier program otherwise produces byte-identical stdout and the same exit
**Console output uses the process streams.** Direct `log`, `info`, and `debug` calls write to stdout; `error` and `warn` write to stderr. These methods work through the global `console` and `node:console` imports. Custom `Console` instances and replacing the global console are not supported yet.
**Stored process streams have a limited native surface.** JavaScript can store `process.stdin`, `process.stdout`, and `process.stderr`, replace output `write` methods, and call saved methods with their stream receiver. Input supports data/end/error listeners, pause/resume, buffered reads, and raw mode on a TTY. Output writes are synchronous and do not model queued backpressure; successful callbacks run on the next-tick queue. Stream piping, encoding changes, arbitrary properties, and other stream events are not supported through these stored values yet. TypeScript output parameters retain the existing `write(string)` surface.
**Process shape** — `process.argv[0]` is `"scriptc"` and `argv[1]` is the binary's path (positions line up with Node; `argv[2]` onward are your args). The uncaught-exception stderr line reads `Uncaught <value>` instead of Node's stack-trace block (exit code and pre-throw stdout are identical). Runtime errors carry `message` and Node's `code`, but not `errno`/`syscall`/`path`.
**Comparator call sequences differ in `sort` and `toSorted`** (scriptc uses a stable bottom-up merge sort, while V8 uses TimSort). Sorted results are byte-identical for consistent comparators. Ordered inputs use a linear number of comparator calls, but merge-buffer movement remains O(n log n) even when every boundary is already ordered. **`localeCompare` compares code units**, not ICU collation.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "13f2c3f789ea7a8500d1",
"artifactVersion": "a063593865cc30056a9c",
"rowCount": 3662
}
+11 -11
View File
@@ -842,9 +842,9 @@
"apiEntries": 119,
"static": {
"supported": 0,
"partial": 27,
"partial": 30,
"refused": 0,
"not-implemented": 92,
"not-implemented": 89,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -67481,9 +67481,9 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Stored JavaScript process streams support native input listeners, pause/resume and buffered reads, synchronous output writes, and output write replacement; other stream operations remain unsupported.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -67527,9 +67527,9 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Stored JavaScript process streams support native input listeners, pause/resume and buffered reads, synchronous output writes, and output write replacement; other stream operations remain unsupported.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -67573,9 +67573,9 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Stored JavaScript process streams support native input listeners, pause/resume and buffered reads, synchronous output writes, and output write replacement; other stream operations remain unsupported.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 47,
"verify-gap": 2568,
"audit-partial": 597,
"verify-gap": 2565,
"audit-partial": 600,
"classify": 2,
"implement": 81
},
@@ -73537,12 +73537,17 @@
"priority": "normal",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:process",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:process.stderr",
"tests": [
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
"status": "partial",
@@ -73595,12 +73600,17 @@
"priority": "normal",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:process",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:process.stdin",
"tests": [
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
"status": "partial",
@@ -73653,12 +73663,17 @@
"priority": "normal",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:process",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:process.stdout",
"tests": [
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
"status": "partial",
@@ -915,9 +915,9 @@
"apiEntries": 119,
"static": {
"supported": 0,
"partial": 27,
"partial": 30,
"refused": 0,
"not-implemented": 92,
"not-implemented": 89,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -86856,7 +86856,11 @@
"tests/corpus/2946-child-fork-ipc/main.ts",
"tests/corpus/990-process-basics.ts",
"tests/corpus/process-builtin-values.js",
"tests/corpus/process-versions-storage.ts"
"tests/corpus/process-versions-storage.ts",
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
@@ -89608,8 +89612,14 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "compiler-feature:process.stderr",
"tests": [
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
"status": "partial",
@@ -89655,8 +89665,14 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "compiler-feature:process.stdin",
"tests": [
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
"status": "partial",
@@ -89702,8 +89718,14 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "compiler-feature:process.stdout",
"tests": [
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-overrides.mjs",
"tests/corpus/stdio-write-errors.mjs",
"tests/harness/event-loop.test.ts"
]
},
"dynamic": {
"status": "partial",
+3
View File
@@ -598,6 +598,9 @@ function publicDetail(tier) {
if (source.startsWith("surface-manifest:")) return "Implemented for the call shapes accepted by the compiler lowering.";
if (source.startsWith("compiler-dedicated:")) return "Implemented by a dedicated static compiler/runtime path.";
if (source.startsWith("compiler-feature:")) {
if (/^compiler-feature:process\.std(?:in|out|err)$/.test(source)) {
return "Stored JavaScript process streams support native input listeners, pause/resume and buffered reads, synchronous output writes, and output write replacement; other stream operations remain unsupported.";
}
if (source === "compiler-feature:url.URL") {
return "Supports absolute inputs and relative inputs with string or URL bases; setters and some WHATWG parsing behavior remain unsupported.";
}
@@ -87,6 +87,7 @@
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/harness/event-loop.test.ts"] },
{ "chapter": "process", "signatures": ["`process.getBuiltinModule(id)`"], "status": "partial", "evidence": ["tests/corpus/process-builtin-values.js", "packages/compiler/test/process-builtin-values.test.ts"] },
{ "chapter": "process", "signatures": ["`versions` Type: {Object}"], "status": "partial", "evidence": ["tests/corpus/process-versions-storage.ts"] },
{ "chapter": "http", "signatures": ["Class: `http.ClientRequest`", "Class: `http.Server`", "Class: `http.ServerResponse`", "Class: `http.IncomingMessage`"], "status": "partial", "evidence": ["tests/corpus/2981-http-static-events.ts", "tests/corpus/2976-http-static-incoming-state.ts"] },
+14 -8
View File
@@ -3436,11 +3436,10 @@ function emitIntrinsicExpr(
emitter.moveTemp(reason); // the cell takes ownership
const rc = vAdapters(t);
if (t.kind === "dyn") {
// The thrown-dyn representation (REF + dyn adapters): catch
// bindings and the unhandled dispatch see the dyn value
// itself — identity preserved.
// Preserve Error classification and checked-value identity
// through promise rejection and catch observers.
emitter.line(
`scr_throw_ref_classified(${reason.name}, &${rc.retain}, &${rc.release}, NULL, scr_dyn_is_object(${reason.name}));${emitter.srcComment(e.loc)}`,
`scr_dyn_throw(${reason.name});${emitter.srcComment(e.loc)}`,
);
} else {
emitter.line(
@@ -3703,9 +3702,11 @@ function emitAsyncExpr(
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(t);
emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
} else if (t.kind === "dyn") {
emitter.line(`scr_dyn_throw(${a.name});${emitter.srcComment(e.loc)}`);
} else if (t.kind === "jsval") {
const rc = vAdapters(t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
const test = "scr_jsval_is_object";
emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`);
} else {
const rc = vAdapters(t);
@@ -3766,9 +3767,11 @@ function emitAsyncExpr(
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(t);
emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
} else if (t.kind === "dyn") {
emitter.line(`scr_dyn_throw(${a.name});${emitter.srcComment(e.loc)}`);
} else if (t.kind === "jsval") {
const rc = vAdapters(t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
const test = "scr_jsval_is_object";
emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`);
} else {
const rc = vAdapters(t);
@@ -8034,6 +8037,9 @@ function emitProcessLibCall(state: LibCallState): Temp {
return finish(`scr_process_cwd()`);
case "process.stdoutWrite":
return finish(`scr_process_stdout_write(${arg(0)})`);
case "process.stdio":
emitter.usesTimers = true;
return finish(`scr_process_stdio(${arg(0)})`);
case "process.stderrWrite":
return finish(`scr_process_stderr_write(${arg(0)})`);
case "process.envGet": {
+4 -2
View File
@@ -727,9 +727,11 @@ function emitStmtBody(emitter: CEmitter, s: IrStmt): void {
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(t);
emitter.line(`scr_throw_primitive_ref(${v.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(s.loc)}`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
} else if (t.kind === "dyn") {
emitter.line(`scr_dyn_throw(${v.name});${emitter.srcComment(s.loc)}`);
} else if (t.kind === "jsval") {
const rc = vAdapters(t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
const test = "scr_jsval_is_object";
emitter.line(`scr_throw_ref_classified(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${v.name}));${emitter.srcComment(s.loc)}`);
} else {
const rc = vAdapters(t);
@@ -2508,9 +2508,12 @@ export class LlEmitter {
const rc = vAdapters(this.shapeHost, t);
this.declare(`declare void @scr_throw_primitive_ref(ptr, ptr, ptr, ptr)`);
B.line(`call void @scr_throw_primitive_ref(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr null)`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
} else if (t.kind === "dyn") {
this.declare(`declare void @scr_dyn_throw(ptr)`);
B.line(`call void @scr_dyn_throw(ptr ${v.name})`);
} else if (t.kind === "jsval") {
const rc = vAdapters(this.shapeHost, t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
const test = "scr_jsval_is_object";
this.declare(`declare zeroext i1 @${test}(ptr)`);
this.declare(`declare void @scr_throw_ref_classified(ptr, ptr, ptr, ptr, i1 zeroext)`);
const object = B.tmp();
@@ -186,6 +186,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.envUnset": "scr_env_unset",
"process.envPairs": "scr_env_pairs",
"process.stdoutWrite": "scr_process_stdout_write",
"process.stdio": "scr_process_stdio",
"process.stderrWrite": "scr_process_stderr_write",
"process.isTTY": "scr_process_is_tty",
"date.now": "scr_date_now",
@@ -890,6 +891,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
};
export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.stdio",
"fetch.start",
"fetch.abortTimeout", "fetch.streamNew", "fetch.streamFrom",
"readable.new", "writable.new", "duplex.new", "transform.new", "passthrough.new",
@@ -6800,6 +6800,13 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (isJsSourceFile(call.getSourceFile())) {
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind === "dyn") {
return { kind: "dynInvoke", recv: receiver, method: name, calleeName: call.expression.getText(),
args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
}
}
if (name === "write" && call.arguments.length === 1) {
const receiver = lowerer.lowerExpr(access.expression);
const data = lowerer.lowerExpr(call.arguments[0]!);
@@ -7056,6 +7063,15 @@ 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;
const direct = ts.isPropertyAccessExpression(expr.expression) ? lowerer.stdlibGlobalMember(expr.expression, "process") : null;
if (isJsSourceFile(expr.getSourceFile()) &&
(direct === "stdin" || direct === "stdout" || direct === "stderr" ||
lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind === "procStream")) {
const value = lowerer.lowerExpr(expr.expression);
if (value.type.kind === "dyn") return {
kind: "dynKeyGet", value, key: strLit(member, locOf(expr.name)), type: DYN, loc: locOf(expr),
};
}
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;
@@ -7242,6 +7258,9 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
// procStream scalar, minted as the stream's fd. Member reads
// (`process.stdout.isTTY`, `.write(...)`) never reach here — their
// OUTER expressions dispatch first.
if (member === "stdin" || ((member === "stdout" || member === "stderr") && isJsSourceFile(expr.getSourceFile()))) {
return { kind: "libCall", fn: "process.stdio", args: [numLit(member === "stdin" ? 0 : member === "stdout" ? 1 : 2, loc)], type: DYN, loc };
}
if (member === "stdout" || member === "stderr") {
return { kind: "numLit", value: member === "stdout" ? 1 : 2, type: PROCSTREAM_T, loc };
}
@@ -7752,6 +7771,14 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
export function lowerProcessMethodCall(lowerer: Lowerer, call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
if (call.questionDotToken) return null;
if (isJsSourceFile(call.getSourceFile()) && ts.isPropertyAccessExpression(access.expression)) {
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
if (stream === "stdin" || stream === "stdout" || stream === "stderr") {
if (call.arguments.some(ts.isSpreadElement)) lowerer.noLowering("process stream spread arguments", call);
return { kind: "dynInvoke", recv: lowerer.lowerExpr(access.expression), method: access.name.text,
calleeName: access.getText(), args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call) };
}
}
const directProcessMember = lowerer.stdlibGlobalMember(access, "process");
if (directProcessMember === "getBuiltinModule") {
if (call.arguments.some(ts.isSpreadElement)) lowerer.noLowering("process.getBuiltinModule with spread arguments", call);
@@ -5899,6 +5899,27 @@ function lowerOptionalStringNumber(
// tsc types the return by the evolved shape, the binding lowered
// dyn): probe the lowering and claim exactly the dyn results.
const probed = tryLowerExpression(lowerer, access.expression);
if (isJsSourceFile(call.getSourceFile()) && probed?.type.kind === "func" &&
!["call", "apply", "bind", "toString", "valueOf", "constructor"].includes(access.name.text) &&
canBoxFuncIntoDyn(probed.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
const loc = locOf(call);
const fnName = jsFuncNameOf(access.expression);
const boxed: IrExpr = { kind: "dynFrom", value: probed, type: DYN, loc, ...(fnName !== null ? { fnName } : {}) };
const local = lowerer.declareHiddenLocal("%callableReceiver", DYN);
const receiver: IrExpr = { kind: "varRef", localId: local.id, type: DYN, loc };
const member: IrExpr = {
kind: "dynKeyGet", value: receiver,
key: { kind: "strLit", value: access.name.text, type: STRING, loc }, type: DYN, loc,
};
if (call.arguments.some(ts.isSpreadElement)) {
lowerer.unsupported("SC1090", call, "spread arguments in callable property calls");
}
return {
kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: boxed, loc }],
result: { kind: "dynCall", callee: member, receiver, calleeName: access.getText(),
args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }, type: DYN, loc,
};
}
if (probed?.type.kind !== "dyn") return null;
recv = probed;
}
@@ -6996,6 +7017,22 @@ function loweredTemplateStrings(
* requireExactArityValue decides whether that completed ABI may escape. */
export function lambdaSignature(lowerer: Lowerer, node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration,): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } {
if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures");
// A JS replacement of a native stream's write method receives the
// actual call tuple, including overloads and omitted arguments. Node's
// contextual overload signature cannot define this closure's ABI.
if (isJsSourceFile(node.getSourceFile()) && !node.type &&
(ts.isFunctionExpression(node) || ts.isArrowFunction(node)) &&
node.parameters.every((param) => !param.type && !param.dotDotDotToken && !param.initializer) &&
ts.isBinaryExpression(node.parent) && node.parent.right === node &&
node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(node.parent.left) && node.parent.left.name.text === "write" &&
lowerer.isStdlibMember(node.parent.left)) {
const receiver = lowerer.lowerExpr(node.parent.left.expression);
if (receiver.type.kind === "dyn") {
const shapes: ParamShape[] = node.parameters.map(() => ({ type: DYN, mode: "required" }));
return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) };
}
}
if (node.typeParameters) {
// Generic function-like forms monomorphize only where a static home
// exists: top-level generic function declarations, generic methods
@@ -9155,6 +9192,10 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
ts.isStringLiteral(keyNode) && keyNode.text === "cause") {
target = lowerer.coerceToExpected(target, DYN);
}
if (target?.type.kind === "func" && lowerer.dynConvertible(target.type)) {
const fnName = jsFuncNameOf(call.arguments[0]!);
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!), ...(fnName !== null ? { fnName } : {}) };
}
if (!target || (target.type.kind !== "dyn" && !isUnitType(target.type))) return null;
if (target.type.kind !== "dyn") target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) };
const key = lowerer.lowerExprExpecting(call.arguments[1]!, DYN);
@@ -9170,7 +9211,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
// _mustCallInner copying name/length onto the mustCall wrapper): the
// runtime turns each descriptor's `value` into an own property on
// the dyn node (OBJ members preserve attributes; FUNC nodes carry an
// own-property table). Accessors throw loudly.
// own-property table). Accessors use the target as their receiver.
// The result is the target, like JS. Typed targets keep the fence:
// static shapes have no property table to extend.
if (member === "defineProperties" && call.arguments.length === 2 &&
@@ -9184,7 +9225,8 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
target && target.type.kind === "func" &&
canBoxFuncIntoDyn(target.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))
) {
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) };
const fnName = jsFuncNameOf(call.arguments[0]!);
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!), ...(fnName !== null ? { fnName } : {}) };
}
if (target && isUnitType(target.type)) {
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) };
@@ -9251,9 +9293,9 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
// A CHECKED-DYNAMIC receiver (the JS file-scope object-literal
// identity story): the runtime dyn probe — OBJ member presence, ARR
// index bounds, Node's ToObject TypeError on nullish.
if (probed?.type.kind === "dyn") {
if (probed?.type.kind === "dyn" || (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) {
const loc = locOf(call);
const receiver = lowerer.lowerExpr(recvNode);
const receiver = lowerer.coerceToExpected(probed, DYN);
const key = ownPropertyKey(lowerer, lowerer.lowerExpr(keyNode));
if (!key) return null;
return { kind: "libCall", fn: "dyn.hasOwn", args: [receiver, key], type: BOOL, loc };
@@ -9331,7 +9373,8 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
// Unit-typed arguments (Object.keys(null)) ride the same runtime
// walk: it throws Node's catchable TypeError.
const isUnit = probed !== null && probed !== undefined && isUnitType(probed.type);
if (isDyn || isUnit) {
const isFunction = probed?.type.kind === "func" && lowerer.dynConvertible(probed.type);
if (isDyn || isUnit || isFunction) {
const fn = member === "keys" ? "dyn.objKeys" : member === "values" ? "dyn.objValues" : "dyn.objEntries";
let v = lowerer.lowerExpr(argNode);
if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) };
@@ -2526,6 +2526,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// instead of misreading the payload (the dyn boundary's usual stance).
// Object/array narrowings stay dyn-typed and keep their fences.
if (expr.type.kind === "dyn") {
if (isJsSourceFile(node.getSourceFile()) && ts.isIdentifier(node)) {
const symbol = lowerer.resolveValueSymbol(node);
if (symbol && lowerer.checker.declarationsOf(symbol).some((decl) => {
if (!ts.isParameter(decl) || decl.type) return false;
const fn = decl.parent;
if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return false;
const assignment = fn.parent;
return ts.isBinaryExpression(assignment) && assignment.right === fn &&
ts.isPropertyAccessExpression(assignment.left) && assignment.left.name.text === "write" &&
lowerer.isStdlibMember(assignment.left);
})) return expr;
}
// Node's TTY declarations promise booleans/numbers even when a pipe
// has no such property. Preserve the checked read for JS capability
// probes instead of treating a declaration as a runtime type guard.
if (isJsSourceFile(node.getSourceFile()) && ts.isPropertyAccessExpression(node) &&
["isTTY", "columns", "rows"].includes(node.name.text) && lowerer.isStdlibMember(node)) return expr;
// A null-initialized JS field can later receive an object even when
// checker flow calls its non-null branch never. The live checked
// value remains authoritative; never is not a numeric guard.
@@ -4429,6 +4446,16 @@ export function lowerOptionalNumber(
let receiverIr = neverTaintedJsType(lowerer, expr.expression, lowerer.typeOf(expr.expression))
? null
: lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
if (isJsSourceFile(expr.getSourceFile())) {
const receiver = tryLowerExpression(lowerer, expr.expression);
if (receiver?.type.kind === "func" && lowerer.dynConvertible(receiver.type)) {
const loc = locOf(expr);
const fnName = jsFuncNameOf(expr.expression);
const boxed: IrExpr = { kind: "dynFrom", value: receiver, type: DYN, loc, ...(fnName !== null ? { fnName } : {}) };
const key = lowerRecordPropertyKey(lowerer, lowerer.lowerExpr(expr.argumentExpression), expr.argumentExpression);
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, value: boxed, type: DYN, loc }, expr);
}
}
// A readonly tuple union under Array.isArray is checker-typed as an
// intersection with any[], whose structural mapping is not the tuple
// shape. maybeNarrow on the receiver uses the runtime tag proof to
@@ -5237,13 +5264,15 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
};
}
/** `a[i] = v` in statement position → arraySet (element writes, like local
* assignment, produce no value in our subset). */
function lowerDynElementAssignment(lowerer: Lowerer, expr: ts.BinaryExpression, recv: IrExpr): IrExpr {
/** Checked member assignment evaluates the reference before its RHS and
* yields the original assigned value, including in a chained assignment. */
function lowerDynMemberAssignment(lowerer: Lowerer, expr: ts.BinaryExpression, recv: IrExpr): IrExpr {
const loc = locOf(expr);
const target = expr.left as ts.ElementAccessExpression;
const target = expr.left as ts.ElementAccessExpression | ts.PropertyAccessExpression;
const receiver = lowerer.declareHiddenLocal("%setReceiver", DYN);
const keyValue = lowerer.lowerExprExpecting(target.argumentExpression, DYN);
const keyValue = ts.isElementAccessExpression(target)
? lowerer.lowerExprExpecting(target.argumentExpression, DYN)
: lowerer.coerceToExpected({ kind: "strLit", value: target.name.text, type: STRING, loc }, DYN);
const key = lowerer.declareHiddenLocal("%setKey", DYN);
const raw = lowerer.lowerExpr(expr.right);
const value: IrExpr = raw.type.kind === "void"
@@ -5267,6 +5296,25 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
};
}
/** JavaScript callable members live on the closure, so aliases and chained
* writes share the same storage as Object.defineProperties. */
export function lowerNativeFunctionAssignment(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null {
const target = expr.left;
if (!isJsSourceFile(expr.getSourceFile()) ||
(!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) ||
target.questionDotToken) return null;
const receiver = tryLowerExpression(lowerer, target.expression);
if (receiver?.type.kind !== "func" ||
!canBoxFuncIntoDyn(receiver.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) return null;
fenceNodeModuleMutation(lowerer, target, "assignment");
const fnName = jsFuncNameOf(target.expression);
const boxed: IrExpr = {
kind: "dynFrom", value: receiver, type: DYN, loc: locOf(target.expression),
...(fnName !== null ? { fnName } : {}),
};
return lowerDynMemberAssignment(lowerer, expr, boxed);
}
export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt {
const target = expr.left as ts.ElementAccessExpression;
fenceNodeModuleMutation(lowerer, target, "assignment");
@@ -5316,7 +5364,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
}
const checkedReceiver = tryLowerExpression(lowerer, target.expression);
if (checkedReceiver?.type.kind === "dyn") {
return { kind: "exprStmt", expr: lowerDynElementAssignment(lowerer, expr, checkedReceiver), loc: locOf(expr) };
return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, checkedReceiver), loc: locOf(expr) };
}
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(target.expression));
if (receiverIr?.kind === "jsval") {
@@ -6316,6 +6364,10 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
if (cacheHas) return cacheHas;
if (op === ts.SyntaxKind.EqualsToken || (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment)) {
if (op === ts.SyntaxKind.EqualsToken) {
const callableWrite = lowerNativeFunctionAssignment(lowerer, expr);
if (callableWrite) return callableWrite;
}
if (op === ts.SyntaxKind.QuestionQuestionEqualsToken ||
op === ts.SyntaxKind.AmpersandAmpersandEqualsToken || op === ts.SyntaxKind.BarBarEqualsToken) {
return lowerShortCircuitAssignment(lowerer, expr);
@@ -6433,7 +6485,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
if (ts.isElementAccessExpression(expr.left) && !expr.left.questionDotToken) {
const recv = tryLowerExpression(lowerer, expr.left.expression);
if (recv?.type.kind === "dyn") {
return lowerDynElementAssignment(lowerer, expr, recv);
return lowerDynMemberAssignment(lowerer, expr, recv);
}
}
// `h.k = v` on an ISLAND receiver in VALUE position: the engine
@@ -8588,6 +8640,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
return { kind: "unionIsTag", unionId: envType.unionId, tag: undefTag, negated: true, value: read, type: BOOL, loc };
}
let recv = lowerer.lowerExpr(expr.right);
if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "func" && lowerer.dynConvertible(recv.type)) {
recv = lowerer.coerceToExpected(recv, DYN);
}
const siteType = lowerer.mapTypeOf(lowerer.typeOf(expr.right));
if (recv.type.kind === "union" && siteType?.kind === "union" && !typeEquals(recv.type, siteType)) {
const helper = lowerer.narrowedRetagHelper(expr.right, recv.type.unionId, siteType.unionId, loc);
@@ -17,7 +17,7 @@ import { invalidJsonModuleDiag, npmEmbedFailedDiag, requiresDynamicImportDiag }
import { BOOL, DYN, F64, IrClassDef, IrExpr, IrFunction, IrGlobal, IrRecordShape, IrStmt, IrType, IrUnionDef, JSVAL, RUNTIME_ERROR_CLASSES, STRING, SrcLoc, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, isUnitType } from "../../ir/ir.js";
import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, staticImportNamespaceType, uncheckedOverloadHandleCall } from "./lowerer.js";
import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, stripTypeCasts } from "./lower-builtins.js";
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js";
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, funcTypeFromParamShapes, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js";
import { hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js";
import { streamClassAliasDecl } from "./lower-stream.js";
import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js";
@@ -831,7 +831,17 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
e.operatorToken.kind === ts.SyntaxKind.BarBarToken || e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) {
return jsDynHoldableInitializer(lowerer, e.left) && jsDynHoldableInitializer(lowerer, e.right);
}
// A member of a checked module value is itself checked, including a
// callable whose inferred overloads cannot describe one native ABI.
if ((ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) &&
ts.isIdentifier(e.expression) && lowerer.globalOf(e.expression)?.type.kind === "dyn") return true;
if (ts.isIdentifier(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
if (ts.isIdentifier(e)) {
if (lowerer.globalOf(e)?.type.kind === "dyn") return true;
const signature = lowerer.isTopLevelFnSymbol(e) ? lowerer.fnSigOf(e) : null;
if (signature && canBoxFuncIntoDyn(funcTypeFromParamShapes(signature.params, signature.returnType),
(id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) return true;
}
const type = lowerer.mapTypeOf(lowerer.typeOf(e)) ?? dynFallbackType(lowerer, e, lowerer.typeOf(e));
if (type?.kind === "func" && canBoxFuncIntoDyn(type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) return true;
}
@@ -31,7 +31,7 @@ import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbse
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import { lowerEnvironmentKey } from "./lower-exprs.js";
import { lowerEnvironmentKey, lowerNativeFunctionAssignment } from "./lower-exprs.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerUnionFieldWrite } from "./expressions/union-field-write.js";
@@ -4503,7 +4503,10 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
loc,
};
}
const obj = lowerer.lowerExpr(target.expression);
let obj = lowerer.lowerExpr(target.expression);
if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "func" && lowerer.dynConvertible(obj.type)) {
obj = lowerer.coerceToExpected(obj, DYN);
}
if (obj.type.kind === "dyn") {
const strict = { kind: "boolLit", value: isStrictDelete(expr), type: BOOL, loc } as const;
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keyDelete", args: [obj, lowerKey(), strict], type: VOID, loc }, loc };
@@ -5047,6 +5050,8 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
};
}
if (opKind === ts.SyntaxKind.EqualsToken) {
const callableWrite = lowerNativeFunctionAssignment(lowerer, expr);
if (callableWrite) return { kind: "exprStmt", expr: callableWrite, loc: locOf(expr) };
// Expando function members (`foo.bar = 12`, `foo[SYM] = v` on a
// module-level function/callable const): the member's module
// global (lower-expando.ts) — claimed by symbol identity before
+7
View File
@@ -3701,6 +3701,7 @@ export type IrLibFn =
* never throws. */
| "process.stdoutWrite"
| "process.stderrWrite"
| "process.stdio"
| "timers.setTimeout"
/** The repeating timer pair. setInterval takes (callback, ms) like
* setTimeout and RETURNS the f64 handle the fallback declarations
@@ -6629,6 +6630,7 @@ export function moduleUsesFetch(mod: IrModule): boolean {
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
* process signal/exit listeners and the piped-stdin surface. */
const PROCESS_EVENT_LIB_FNS: ReadonlySet<string> = new Set([
"process.stdio",
"process.onSignal",
"process.offSignal",
"process.onExit",
@@ -7607,6 +7609,11 @@ export function moduleLibNondeterministicSurface(mod: IrModule): string | null {
* seed on `dynCheck` and `awaitExpr` nodes, which throw on validation
* failure / promise rejection). */
export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.stdoutWrite",
"process.stderrWrite",
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
+1
View File
@@ -1077,6 +1077,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"process.kill": { argTypes: [F64, STRING], result: BOOL },
"process.killNum": { argTypes: [F64, F64], result: BOOL },
"process.stdoutWrite": { argTypes: [STRING], result: BOOL },
"process.stdio": { argTypes: [F64], result: DYN },
"process.stderrWrite": { argTypes: [STRING], result: BOOL },
// error.new's result and the receiver slots are builtin-error classes —
// program-dependent object types, checked in the libCall case.
@@ -11385,6 +11385,48 @@
"<repo>/tests/diagnostics/union-clone-override-width.ts"
],
"diags": []
},
"<repo>/tests/corpus/callable-properties.mjs": {
"order": [
"<repo>/tests/corpus/callable-properties.mjs"
],
"diags": []
},
"<repo>/tests/corpus/callable-property-cycles.js": {
"order": [
"<repo>/tests/corpus/callable-property-cycles.js"
],
"diags": []
},
"<repo>/tests/corpus/stdio-values.mjs": {
"order": [
"<repo>/tests/corpus/stdio-values.mjs"
],
"diags": []
},
"<repo>/tests/corpus/stdio-write-errors.mjs": {
"order": [
"<repo>/tests/corpus/stdio-write-errors.mjs"
],
"diags": []
},
"<repo>/tests/corpus/stdio-write-overrides.mjs": {
"order": [
"<repo>/tests/corpus/stdio-write-overrides.mjs"
],
"diags": []
},
"<repo>/tests/corpus/boxed-error-rethrows.mjs": {
"order": [
"<repo>/tests/corpus/boxed-error-rethrows.mjs"
],
"diags": []
},
"<repo>/tests/corpus/callable-module-members.mjs": {
"order": [
"<repo>/tests/corpus/callable-module-members.mjs"
],
"diags": []
}
}
}
+2 -4
View File
@@ -163,14 +163,12 @@ ScrBytes *scr_crypto_random_bytes(double n) {
bool scr_process_stdout_write_bytes(const ScrBytes *b, const ScrStr *encoding) {
(void)encoding;
scr_stdio_write(1, b->data, b->len * scr_bytes_elem_size(b->elem));
return true;
return scr_stdio_write(1, b->data, b->len * scr_bytes_elem_size(b->elem));
}
bool scr_process_stderr_write_bytes(const ScrBytes *b, const ScrStr *encoding) {
(void)encoding;
scr_stdio_write(2, b->data, b->len * scr_bytes_elem_size(b->elem));
return true;
return scr_stdio_write(2, b->data, b->len * scr_bytes_elem_size(b->elem));
}
/* Buffer.from over checked-native input. Strings use the existing codec;
+49 -3
View File
@@ -3,6 +3,8 @@
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#include <fcntl.h> /* _O_BINARY */
#include <io.h> /* _setmode, _fileno */
@@ -150,7 +152,44 @@ void scr_init(void) {
/* ONE formatter for both console streams — console.error/warn print
* byte-identically to console.log in Node (same inspect rendering), only
* the stream differs. */
bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len);
static void scr_console_write(FILE *out, size_t n, const ScrLogArg *args) {
if (scr_stdio_write_hook) {
size_t capacity = 1;
for (size_t i = 0; i < n; i++) {
size_t length = args[i].tag == SCR_ARG_STR ? args[i].v.s->len : 32;
if (length >= SIZE_MAX - capacity) scr_trap("scriptc: console output too large\n");
capacity += length + 1;
}
char *line = malloc(capacity);
if (!line) scr_trap("scriptc: out of memory\n");
size_t used = 0;
for (size_t i = 0; i < n; i++) {
if (i) line[used++] = ' ';
const ScrLogArg *arg = &args[i];
if (arg->tag == SCR_ARG_STR) {
memcpy(line + used, arg->v.s->data, arg->v.s->len);
used += arg->v.s->len;
} else if (arg->tag == SCR_ARG_BOOL) {
const char *text = arg->v.b ? "true" : "false";
size_t length = arg->v.b ? 4 : 5;
memcpy(line + used, text, length);
used += length;
} else if (arg->v.f == 0 && signbit(arg->v.f)) {
memcpy(line + used, "-0", 2);
used += 2;
} else {
used += scr_f64_to_str(arg->v.f, line + used);
}
}
line[used++] = '\n';
scr_stdio_write_hook(out == stderr ? 2 : 1, line, used);
free(line);
/* The global Node console uses ignoreErrors=true for stream writes. */
if (scr_exc_pending()) scr_exc_clear();
return;
}
char numbuf[32];
for (size_t i = 0; i < n; i++) {
if (i > 0) fputc(' ', out);
@@ -203,9 +242,16 @@ void scr_console_error(size_t n, const ScrLogArg *args) {
*
* stderr flushes any runtime-internal stdout fragment first to preserve the
* existing merged-fd ordering convention. */
void scr_stdio_write(int fd, const void *data, size_t len) {
int scr_stdio_write_raw(int fd, const void *data, size_t len) {
FILE *out = fd == 2 ? stderr : stdout;
if (fd == 2) fflush(stdout);
if (len > 0) fwrite(data, 1, len, out);
fflush(out);
if (len > 0 && fwrite(data, 1, len, out) != len) return errno ? errno : EIO;
if (fflush(out) != 0) return errno ? errno : EIO;
return 0;
}
bool scr_stdio_write(int fd, const void *data, size_t len) {
if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len);
(void)scr_stdio_write_raw(fd, data, len);
return true;
}
+3 -28
View File
@@ -1179,37 +1179,12 @@ ScrDyn *scr_dyn_define_props(ScrDyn *target, ScrDyn *descs) {
ScrDyn *pair = pending->v.arr.items[i];
ScrDyn *key = pair->v.arr.items[0];
ScrDyn *descriptor = pair->v.arr.items[1];
if (target->kind == SCR_DYN_FUNC &&
(scr_dyn_obj_get(descriptor, "get", 3) || scr_dyn_obj_get(descriptor, "set", 3))) {
scr_throw_error_msg(SCR_ERR_ERROR,
"accessor (get/set) property descriptors on a dynamic value are not supported yet",
strlen("accessor (get/set) property descriptors on a dynamic value are not supported yet"));
ScrDyn *defined = scr_dyn_define_property(target, key, descriptor);
if (!defined) {
scr_dyn_release(pending);
return NULL;
}
if (target->kind == SCR_DYN_OBJ) {
ScrDyn *defined = scr_dyn_define_property(target, key, descriptor);
if (!defined) {
scr_dyn_release(pending);
return NULL;
}
scr_dyn_release(defined);
} else {
if (!target->v.fn.clo->props) {
ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, &scr_dyn_trace_v);
ScrDyn *table = scr_dyn_new_obj();
scr_box_set_ref(box, table); /* the box owns the fresh table */
target->v.fn.clo->props = box;
}
ScrDyn *table = (ScrDyn *)scr_box_get_ref(target->v.fn.clo->props); /* +1 */
ScrDyn *defined = scr_dyn_define_property(table, key, descriptor);
scr_dyn_release(table);
if (!defined) {
scr_dyn_release(pending);
return NULL;
}
scr_dyn_release(defined);
}
scr_dyn_release(defined);
}
scr_dyn_release(pending);
return scr_dyn_retain(target);
+437 -2
View File
@@ -17,6 +17,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#ifdef _WIN32
/* The Windows arm: CRT signal() stands in for sigaction (msvcrt routes
* Ctrl-C/Ctrl-Break through its own SetConsoleCtrlHandler onto a separate
@@ -341,6 +342,11 @@ static size_t scr_stdin_nerr = 0, scr_stdin_err_cap = 0;
static ScrPromise *scr_stdin_waiter = NULL; /* parked for-await next() */
static bool scr_stdin_eof = false;
static bool scr_stdin_destroyed = false;
static bool scr_stdin_paused = false;
static bool scr_stdin_resumed = false;
static bool scr_stdin_read_eof = false;
static ScrBytes *scr_stdin_buffer;
static size_t scr_stdin_read_demand;
void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *),
bool once) {
@@ -416,7 +422,9 @@ bool scr_stdin_ended(void) { return scr_stdin_eof || scr_stdin_destroyed; }
* exits with them registered). */
static bool scr_stdin_watching(void) {
return !scr_stdin_eof && !scr_stdin_destroyed &&
(scr_stdin_ndata > 0 || scr_stdin_waiter != NULL);
((!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) ||
scr_stdin_waiter != NULL ||
(!scr_stdin_read_eof && scr_stdin_read_demand > (scr_stdin_buffer ? scr_stdin_buffer->len : 0)));
}
bool scr_stdin_pending(void) { return scr_stdin_watching(); }
@@ -477,6 +485,9 @@ static void scr_stdin_finish(bool fire_end) {
void scr_stdin_destroy(void) {
if (scr_stdin_destroyed) return;
scr_stdin_destroyed = true;
scr_bytes_release(scr_stdin_buffer);
scr_stdin_buffer = NULL;
scr_stdin_read_demand = 0;
scr_stdin_settle_done();
scr_stdin_drop_listeners();
}
@@ -503,8 +514,21 @@ ScrPromise *scr_stdin_next_chunk(void) {
* with no listener, to the parked for-await promise. Node's stream would
* buffer a chunk nobody consumes; this slice never reads without a
* consumer, so the pipe itself is the buffer (backpressure, not loss). */
static void scr_stdin_deliver(ScrBytes *chunk);
static void scr_stdin_service(void) {
if (!scr_stdin_watching()) return;
if (!scr_stdin_paused && scr_stdin_buffer) {
ScrBytes *chunk = scr_stdin_buffer;
scr_stdin_buffer = NULL;
scr_stdin_deliver(chunk);
scr_bytes_release(chunk);
return;
}
if (scr_stdin_read_eof) {
if (!scr_stdin_buffer) scr_stdin_finish(true);
return;
}
#ifdef _WIN32
/* The poll(2)-probe's win32 spelling, by handle type: a PIPE (the
* harness/ssh shape) answers PeekNamedPipe — bytes available, or a
@@ -564,11 +588,29 @@ static void scr_stdin_service(void) {
return;
}
if (n == 0) {
scr_stdin_finish(true); /* EOF: 'end' fires, iteration ends */
scr_stdin_read_eof = true;
if (!scr_stdin_buffer) scr_stdin_finish(true);
return;
}
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
memcpy(chunk->data, buf, (size_t)n);
if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) {
if (scr_stdin_buffer) {
size_t before = scr_stdin_buffer->len;
ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + (size_t)n));
memcpy(joined->data, scr_stdin_buffer->data, before);
memcpy(joined->data + before, buf, (size_t)n);
scr_bytes_release(scr_stdin_buffer);
scr_bytes_release(chunk);
scr_stdin_buffer = joined;
} else scr_stdin_buffer = chunk;
return;
}
scr_stdin_deliver(chunk);
scr_bytes_release(chunk);
}
static void scr_stdin_deliver(ScrBytes *chunk) {
if (scr_stdin_ndata > 0) {
size_t nd = scr_stdin_ndata;
ScrStdinDataL *snap = malloc(nd * sizeof *snap);
@@ -600,7 +642,397 @@ static void scr_stdin_service(void) {
scr_bytes_release_v, NULL);
scr_promise_release(p);
}
}
/* First-class process streams share the same fd identities and input loop
* as direct process calls. Their method values are native closures, so a
* saved write method can be invoked with .call after write is replaced. */
typedef struct {
int fd;
ScrDyn *write;
ScrDyn **errors;
bool *error_once;
size_t nerrors;
ScrDyn *write_error;
} ScrStdio;
static ScrStdio scr_stdio_streams[3] = {{.fd = 0}, {.fd = 1}, {.fd = 2}};
enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME,
STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_METHOD_COUNT };
static const char *const scr_stdio_names[] = {
"write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy"
};
static ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT];
static bool scr_stdio_initialized;
static bool scr_stdio_raw;
static ScrDyn *scr_stdio_refusal(const char *name) {
char message[192];
int length = snprintf(message, sizeof message, "Native process stream %s has no lowering [SC2020]", name);
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020");
return NULL;
}
static ScrDyn *scr_stdio_listener(ScrClosure *cb) { return scr_box_get_ref(cb->caps[0]); }
static void scr_stdio_fire(ScrClosure *cb, ScrDyn *arg, size_t argc) {
ScrDyn *fn = scr_stdio_listener(cb);
scr_dyn_this_push(&scr_stdio_streams[0], SCR_DYNH_STDIO);
ScrDyn *result = scr_dyn_call(fn, &arg, argc, "stdin listener");
scr_dyn_this_pop();
scr_dyn_release(result);
scr_dyn_release(fn);
}
static void scr_stdio_data(ScrClosure *cb, ScrBytes *bytes) {
ScrDyn *chunk = scr_dyn_new_chunk(bytes);
scr_stdio_fire(cb, chunk, 1);
scr_dyn_release(chunk);
}
static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); }
static void scr_stdio_error(ScrClosure *cb, ScrStr *message) {
ScrError *error = scr_error_new(SCR_ERR_ERROR, message);
ScrDyn *arg = scr_dyn_from_error(error);
scr_error_release(error);
scr_stdio_fire(cb, arg, 1);
scr_dyn_release(arg);
}
static ScrClosure *scr_stdio_callback(ScrDyn *fn, void *fire) {
ScrClosure *cb = scr_closure_new(fire, 1);
cb->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(cb->caps[0], scr_dyn_retain(fn));
return cb;
}
static bool scr_stdio_matches(ScrClosure *cb, void *fire, ScrDyn *fn) {
if (cb->fn != fire) return false;
ScrDyn *registered = scr_stdio_listener(cb);
bool matches = scr_dyn_strict_eq(registered, fn);
scr_dyn_release(registered);
return matches;
}
static void scr_stdio_write_done(ScrClosure *cb) {
ScrDyn *fn = scr_stdio_listener(cb);
ScrDyn *arg = scr_dyn_new_null();
ScrDyn *result = scr_dyn_call(fn, &arg, 1, "write callback");
scr_dyn_release(result);
scr_dyn_release(fn);
scr_dyn_release(arg);
}
static void scr_stdio_write_failed(ScrClosure *cb) {
ScrDyn *callback = scr_box_get_ref(cb->caps[0]);
ScrDyn *error = scr_box_get_ref(cb->caps[1]);
ScrStdio *stream = &scr_stdio_streams[(int)scr_box_get_f64(cb->caps[2])];
if (callback->kind == SCR_DYN_FUNC) {
ScrDyn *result = scr_dyn_call(callback, &error, 1, "write callback");
scr_dyn_release(result);
}
if (!scr_exc_pending()) {
size_t count = stream->nerrors;
ScrDyn **listeners = count ? malloc(count * sizeof *listeners) : NULL;
if (count && !listeners) scr_events_oom();
size_t kept = 0;
for (size_t i = 0; i < count; i++) {
listeners[i] = scr_dyn_retain(stream->errors[i]);
if (stream->error_once[i]) scr_dyn_release(stream->errors[i]);
else {
stream->errors[kept] = stream->errors[i];
stream->error_once[kept++] = false;
}
}
stream->nerrors = kept;
if (!count) scr_throw_ref(scr_dyn_retain(error), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
for (size_t i = 0; i < count; i++) {
if (!scr_exc_pending()) {
scr_dyn_this_push(stream, SCR_DYNH_STDIO);
ScrDyn *result = scr_dyn_call(listeners[i], &error, 1, "error listener");
scr_dyn_this_pop();
scr_dyn_release(result);
}
scr_dyn_release(listeners[i]);
}
free(listeners);
}
scr_dyn_release(callback);
scr_dyn_release(error);
}
static void scr_stdio_queue_write_error(ScrStdio *stream, ScrDyn *callback, int code) {
if (!stream->write_error) {
const char *name = code == EPIPE ? "EPIPE" : code == EBADF ? "EBADF" : "EIO";
char message[64];
int length = snprintf(message, sizeof message, "write %s", name);
ScrStr *text = scr_str_new(message, (size_t)length);
ScrError *error = scr_error_new(SCR_ERR_ERROR, text);
scr_str_release(text);
scr_error_set_code(error, name);
stream->write_error = scr_dyn_from_error(error);
scr_error_release(error);
}
ScrClosure *cb = scr_closure_new((void *)scr_stdio_write_failed, 3);
cb->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(cb->caps[0], scr_dyn_retain(callback));
cb->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(cb->caps[1], scr_dyn_retain(stream->write_error));
cb->caps[2] = scr_box_new(SCR_BOX_F64);
scr_box_set_f64(cb->caps[2], stream->fd);
scr_next_tick(cb);
}
static void scr_stdio_finish_read(void) {
if (scr_stdin_read_eof && !scr_stdin_buffer && !scr_stdin_eof && !scr_stdin_destroyed)
scr_stdin_finish(true);
}
static ScrDyn *scr_stdio_read(ScrDyn *size) {
bool all = size->kind == SCR_DYN_UNDEF;
if (!all && (size->kind != SCR_DYN_NUM || !isfinite(size->v.num) ||
size->v.num < 0 || size->v.num > 1073741824 || floor(size->v.num) != size->v.num))
return scr_stdio_refusal("read size");
size_t available = scr_stdin_buffer ? scr_stdin_buffer->len : 0;
size_t wanted = all ? available : (size_t)size->v.num;
if (scr_stdin_read_eof && wanted > available) wanted = available;
if (wanted == 0 || wanted > available) {
if (!scr_stdin_eof && !scr_stdin_destroyed) {
scr_stdin_read_demand = wanted > 65536 ? wanted : 65536;
if (scr_stdin_read_eof) scr_next_tick_raw(scr_stdio_finish_read);
}
return scr_dyn_new_null();
}
ScrBytes *chunk = scr_bytes_slice(scr_stdin_buffer, 0, (double)wanted);
ScrBytes *rest = wanted < available ? scr_bytes_slice(scr_stdin_buffer, (double)wanted, (double)available) : NULL;
scr_bytes_release(scr_stdin_buffer);
scr_stdin_buffer = rest;
scr_stdin_read_demand = 65536;
scr_stdin_deliver(chunk);
ScrDyn *result = scr_dyn_new_chunk(chunk);
scr_bytes_release(chunk);
if (scr_stdin_read_eof && !rest) scr_next_tick_raw(scr_stdio_finish_read);
return result;
}
static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc) {
int method = (int)scr_box_get_f64(cb->caps[0]);
ScrDyn *self = scr_dyn_this_get();
if (self->kind != SCR_DYN_HANDLE || self->v.handle.tag != SCR_DYNH_STDIO) {
scr_dyn_release(self);
return scr_stdio_refusal("method receiver");
}
ScrStdio *stream = self->v.handle.ptr;
ScrDyn *arg = argc ? args[0] : scr_dyn_undefined();
ScrDyn *result = NULL;
if (method == STDIO_WRITE && stream->fd != 0) {
ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined();
ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined();
if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); }
if (callback->kind != SCR_DYN_UNDEF && callback->kind != SCR_DYN_NULL && callback->kind != SCR_DYN_FUNC) {
scr_dyn_arg_type_fail("cb", "of type function", callback);
goto done;
}
ScrBytes *bytes = NULL;
if (arg->kind == SCR_DYN_STR) {
if (encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && encoding->kind != SCR_DYN_STR) {
result = scr_stdio_refusal("write encoding");
goto done;
}
ScrStr *enc = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4);
bytes = scr_bytes_from_str(arg->v.str, enc);
scr_str_release(enc);
} else if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem == SCR_BYTES_U8) {
bytes = scr_bytes_retain(arg->v.bytes);
} else {
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", arg);
goto done;
}
if (!bytes) goto done;
int error = scr_stdio_write_raw(stream->fd, bytes->data, bytes->len);
scr_bytes_release(bytes);
if (error) {
scr_stdio_queue_write_error(stream, callback, error);
result = scr_dyn_new_bool(false);
goto done;
}
if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done));
result = scr_dyn_new_bool(true);
} else if (method == STDIO_ON || method == STDIO_ONCE || method == STDIO_OFF) {
ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined();
if (listener->kind != SCR_DYN_FUNC) {
scr_dyn_arg_type_fail("listener", "of type function", listener);
goto done;
}
const char *event = arg->kind == SCR_DYN_STR ? arg->v.str->data : "";
bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0;
bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0;
bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0;
if ((!data && !end && !error) || (stream->fd != 0 && !error)) {
result = scr_stdio_refusal("event");
goto done;
}
bool remove = method == STDIO_OFF, once = method == STDIO_ONCE;
if (stream->fd == 0) {
if (!remove) {
if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once);
if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once);
if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once);
} else {
#define REMOVE_STDIO_LISTENER(list, count, fire) \
for (size_t i = count; i > 0; i--) { \
if (scr_stdio_matches(list[i - 1].cb, (void *)fire, listener)) { \
scr_closure_release(list[i - 1].cb); \
memmove(list + i - 1, list + i, (count - i) * sizeof *list); \
count--; break; \
} \
}
if (data) { REMOVE_STDIO_LISTENER(scr_stdin_data, scr_stdin_ndata, scr_stdio_data); }
if (end) { REMOVE_STDIO_LISTENER(scr_stdin_end, scr_stdin_nend, scr_stdio_end); }
if (error) { REMOVE_STDIO_LISTENER(scr_stdin_err, scr_stdin_nerr, scr_stdio_error); }
#undef REMOVE_STDIO_LISTENER
}
} else if (remove) {
for (size_t i = stream->nerrors; i > 0; i--) {
if (scr_dyn_strict_eq(stream->errors[i - 1], listener)) {
scr_dyn_release(stream->errors[i - 1]);
memmove(stream->errors + i - 1, stream->errors + i, (stream->nerrors - i) * sizeof *stream->errors);
memmove(stream->error_once + i - 1, stream->error_once + i, (stream->nerrors - i) * sizeof *stream->error_once);
stream->nerrors--; break;
}
}
} else {
stream->errors = realloc(stream->errors, (stream->nerrors + 1) * sizeof *stream->errors);
stream->error_once = realloc(stream->error_once, (stream->nerrors + 1) * sizeof *stream->error_once);
if (!stream->errors || !stream->error_once) scr_events_oom();
stream->errors[stream->nerrors] = scr_dyn_retain(listener);
stream->error_once[stream->nerrors++] = once;
}
result = scr_dyn_retain(self);
} else if (stream->fd == 0 && (method == STDIO_PAUSE || method == STDIO_RESUME)) {
scr_stdin_paused = method == STDIO_PAUSE;
scr_stdin_resumed = method == STDIO_RESUME;
result = scr_dyn_retain(self);
} else if (stream->fd == 0 && method == STDIO_IS_PAUSED) {
result = scr_dyn_new_bool(scr_stdin_paused);
} else if (stream->fd == 0 && method == STDIO_READ) {
result = scr_stdio_read(arg);
} else if (stream->fd == 0 && method == STDIO_RAW) {
scr_process_stdin_set_raw_mode(scr_dyn_truthy(arg));
if (!scr_exc_pending()) { scr_stdio_raw = scr_dyn_truthy(arg); result = scr_dyn_retain(self); }
} else if (stream->fd == 0 && method == STDIO_DESTROY && argc == 0) {
scr_stdin_destroy();
result = scr_dyn_retain(self);
} else result = scr_stdio_refusal(scr_stdio_names[method]);
done:
scr_dyn_release(self);
return result;
}
static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) {
ScrStdio *stream = ptr;
if (length == 2 && memcmp(key, "fd", 2) == 0) return scr_dyn_new_num(stream->fd);
if (length == 5 && memcmp(key, "isTTY", 5) == 0)
return scr_process_is_tty(stream->fd) ? scr_dyn_new_bool(true) : scr_dyn_retain(scr_dyn_undefined());
if ((length == 7 && memcmp(key, "columns", 7) == 0) || (length == 4 && memcmp(key, "rows", 4) == 0)) {
double dimension = stream->fd == 0 ? -1 : length == 7 ? scr_process_columns(stream->fd) : scr_process_rows(stream->fd);
return dimension < 0 ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(dimension);
}
if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0)
return scr_dyn_new_num(scr_stdin_buffer ? (double)scr_stdin_buffer->len : 0);
if (stream->fd == 0 && length == 5 && memcmp(key, "isRaw", 5) == 0)
return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_stdio_raw) : scr_dyn_retain(scr_dyn_undefined());
if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed);
if (length == 3 && memcmp(key, "off", 3) == 0) return scr_dyn_retain(scr_stdio_methods[STDIO_OFF]);
if (length == 11 && memcmp(key, "addListener", 11) == 0) return scr_dyn_retain(scr_stdio_methods[STDIO_ON]);
for (int i = 0; i < STDIO_METHOD_COUNT; i++) {
if (strlen(scr_stdio_names[i]) != length || memcmp(key, scr_stdio_names[i], length) != 0) continue;
if (i == STDIO_WRITE) return stream->fd == 0 ? NULL : scr_dyn_retain(stream->write ? stream->write : scr_stdio_methods[i]);
if (i == STDIO_RAW && !scr_process_is_tty(stream->fd)) return NULL;
if (i >= STDIO_PAUSE && stream->fd != 0) return scr_stdio_refusal(scr_stdio_names[i]);
return scr_dyn_retain(scr_stdio_methods[i]);
}
return NULL;
}
static bool scr_stdio_set(void *ptr, const char *key, size_t length, const ScrDyn *value) {
ScrStdio *stream = ptr;
if (stream->fd == 0 || length != 5 || memcmp(key, "write", 5) != 0) return false;
ScrDyn *old = stream->write;
stream->write = scr_dyn_retain((ScrDyn *)value);
scr_dyn_release(old);
return true;
}
static ScrDyn *scr_stdio_invoke(void *ptr, ScrDyn *self, const char *name,
ScrDyn *const *args, size_t argc, const char *what) {
ScrDyn *fn = scr_stdio_get(ptr, name, strlen(name));
if (scr_exc_pending()) return NULL;
scr_dyn_this_push_dyn(self);
ScrDyn *result = scr_dyn_call(fn ? fn : scr_dyn_undefined(), args, argc, what);
scr_dyn_this_pop();
scr_dyn_release(fn);
return result;
}
static bool scr_stdio_intercept(int fd, const void *data, size_t length) {
ScrStdio *stream = &scr_stdio_streams[fd == 2 ? 2 : 1];
if (!stream->write || scr_dyn_strict_eq(stream->write, scr_stdio_methods[STDIO_WRITE])) {
(void)scr_stdio_write_raw(fd, data, length);
return true;
}
ScrStr *text = scr_str_new(data, length);
ScrDyn *chunk = scr_dyn_new_str(text);
scr_str_release(text);
scr_dyn_this_push(stream, SCR_DYNH_STDIO);
ScrDyn *fn = scr_dyn_retain(stream->write);
ScrDyn *result = scr_dyn_call(fn, &chunk, 1, "write");
scr_dyn_release(fn);
scr_dyn_this_pop();
bool accepted = result && scr_dyn_truthy(result);
scr_dyn_release(result);
scr_dyn_release(chunk);
return accepted;
}
static void scr_stdio_cleanup(void) {
scr_stdio_write_hook = NULL;
for (int i = 0; i < 3; i++) {
ScrStdio *stream = &scr_stdio_streams[i];
scr_dyn_release(stream->write);
stream->write = NULL;
scr_dyn_release(stream->write_error);
stream->write_error = NULL;
for (size_t j = 0; j < stream->nerrors; j++) scr_dyn_release(stream->errors[j]);
free(stream->errors); free(stream->error_once);
stream->errors = NULL; stream->error_once = NULL; stream->nerrors = 0;
}
for (int i = 0; i < STDIO_METHOD_COUNT; i++) {
scr_dyn_release(scr_stdio_methods[i]);
scr_stdio_methods[i] = NULL;
}
scr_stdio_initialized = false;
}
static void *scr_stdio_retain(void *ptr) { return ptr; }
static void scr_stdio_release(void *ptr) { (void)ptr; }
ScrDyn *scr_process_stdio(double fd) {
if (!scr_stdio_initialized) {
scr_stdio_initialized = true;
static const ScrDynHandleOps ops = {
.cls = "ProcessStream", .retain = scr_stdio_retain, .release = scr_stdio_release,
.get = scr_stdio_get, .set = scr_stdio_set, .invoke = scr_stdio_invoke,
};
scr_dyn_handle_install(SCR_DYNH_STDIO, &ops);
for (int i = 0; i < STDIO_METHOD_COUNT; i++) {
ScrClosure *cb = scr_closure_new(NULL, 1);
cb->caps[0] = scr_box_new(SCR_BOX_F64);
scr_box_set_f64(cb->caps[0], i);
scr_stdio_methods[i] = scr_dyn_new_func(cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", scr_stdio_names[i]);
}
scr_stdio_write_hook = scr_stdio_intercept;
}
return scr_dyn_new_handle(&scr_stdio_streams[fd == 0 ? 0 : fd == 2 ? 2 : 1], SCR_DYNH_STDIO);
}
/* ── the process 'exit' event ─────────────────────────────────────────
@@ -741,6 +1173,9 @@ static void scr_events_cleanup_atexit(void) {
scr_sig_cleanup();
scr_stdin_settle_done();
scr_stdin_drop_listeners();
scr_bytes_release(scr_stdin_buffer);
scr_stdin_buffer = NULL;
if (scr_stdio_initialized) scr_stdio_cleanup();
}
void scr_events_install(void) {
+160 -42
View File
@@ -971,10 +971,8 @@ ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) {
if (value->v.fn.clo->props) {
ScrDyn *table = (ScrDyn *)scr_box_get_ref(value->v.fn.clo->props);
ScrDyn *desc = table ? scr_dyn_own_descriptor(table, key) : NULL;
bool found = desc && desc->kind != SCR_DYN_UNDEF;
scr_dyn_release(table);
if (found || scr_exc_pending()) return desc;
scr_dyn_release(desc);
return desc;
}
bool name = key->len == 4 && memcmp(key->data, "name", 4) == 0;
bool length = key->len == 6 && memcmp(key->data, "length", 6) == 0;
@@ -1948,6 +1946,46 @@ static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) {
for (size_t i = 0; i < 6; i++) scr_dyn_release(fields[i]);
}
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key);
static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver);
/* Property tables belong to closures, not temporary checked wrappers. Seed
* the configurable, non-writable builtin members once so redefinitions and
* deletions retain the same behavior through every alias. Returns +1. */
static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) {
if (!function->v.fn.clo->props) {
ScrDyn *table = scr_dyn_new_obj();
const char *name = function->v.fn.name ? function->v.fn.name : "";
ScrStr *name_string = scr_str_new(name, strlen(name));
scr_dyn_obj_set(table, "name", 4, scr_dyn_new_str(name_string));
scr_str_release(name_string);
scr_dyn_obj_set(table, "length", 6, scr_dyn_new_num((double)function->v.fn.arity));
for (size_t i = 0; i < table->v.obj.len; i++) {
table->v.obj.entries[i].writable = false;
table->v.obj.entries[i].enumerable = false;
}
ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, &scr_dyn_trace_v);
scr_box_set_ref(box, table);
function->v.fn.clo->props = box;
}
return (ScrDyn *)scr_box_get_ref(function->v.fn.clo->props);
}
/* Native call/apply/bind lowering still owns these prototype operations.
* Refuse overrides until every typed call site observes the property table. */
static bool scr_dyn_fn_property_fence(const ScrStr *key) {
static const char *const reserved[] = {
"call", "apply", "bind", "toString", "valueOf", "toLocaleString",
"hasOwnProperty", "propertyIsEnumerable", "isPrototypeOf",
"constructor", "prototype", "caller", "arguments", "__proto__",
};
for (size_t i = 0; i < sizeof reserved / sizeof reserved[0]; i++) {
if (key->len != strlen(reserved[i]) || memcmp(key->data, reserved[i], key->len) != 0) continue;
static const char message[] = "scriptc: overriding builtin function properties is not supported";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return true;
}
return false;
}
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) {
if (target->kind == SCR_DYN_PROXY || descriptor->kind == SCR_DYN_PROXY) {
@@ -1958,6 +1996,21 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor)
if (!scr_exc_pending()) scr_dyn_isl_fence(key, "Object.defineProperty key");
if (!scr_exc_pending()) scr_dyn_isl_fence(descriptor, "Object.defineProperty descriptor");
if (scr_exc_pending()) return NULL;
if (target->kind == SCR_DYN_FUNC) {
if (scr_dyn_class_reflection_fence(target)) return NULL;
ScrStr *name = scr_dyn_property_key(key);
if (!name) return NULL;
if (scr_dyn_fn_property_fence(name)) { scr_str_release(name); return NULL; }
ScrDyn *property = scr_dyn_new_str(name);
scr_str_release(name);
ScrDyn *table = scr_dyn_fn_properties(target);
ScrDyn *defined = scr_dyn_define_property(table, property, descriptor);
scr_dyn_release(property);
scr_dyn_release(table);
if (!defined) return NULL;
scr_dyn_release(defined);
return scr_dyn_retain(target);
}
if (target->kind != SCR_DYN_OBJ) {
static const char msg[] = "Object.defineProperty called on non-object";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
@@ -2166,6 +2219,20 @@ bool scr_dyn_is_object(const ScrDyn *d) {
return object;
}
void scr_dyn_throw(ScrDyn *d) {
/* Only actual boxed Errors qualify: a user object's name or properties
* cannot manufacture an Error instance. The cache preserves identity
* when a caught native Error is boxed again. */
ScrError *error = scr_errdyn_err_of(d);
if (error) {
scr_throw_obj(error, scr_error_retain_v, scr_error_release_v, scr_error_trace_arg());
scr_dyn_release(d);
return;
}
scr_throw_ref_classified(d, scr_dyn_retain_v, scr_dyn_release_v,
scr_dyn_trace_v, scr_dyn_is_object(d));
}
ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Symbol.toStringTag lookup");
@@ -2973,6 +3040,22 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
return out;
}
if (v->kind == SCR_DYN_OBJ) return scr_dyn_obj_get(v, key->data, key->len) != NULL;
if (v->kind == SCR_DYN_FUNC) {
ScrDyn *descriptor = scr_dyn_own_descriptor(v, key);
bool present = descriptor && descriptor->kind != SCR_DYN_UNDEF;
scr_dyn_release(descriptor);
if (!present && !scr_exc_pending()) {
static const char *const inherited[] = {
"name", "length", "call", "apply", "bind", "toString", "valueOf", "toLocaleString",
"hasOwnProperty", "propertyIsEnumerable", "isPrototypeOf", "constructor", "__proto__",
"caller", "arguments",
};
for (size_t i = 0; i < sizeof inherited / sizeof inherited[0]; i++) {
if (key->len == strlen(inherited[i]) && memcmp(key->data, inherited[i], key->len) == 0) return true;
}
}
return present;
}
if (v->kind == SCR_DYN_ARR) {
if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return true;
if (key->len == 0 || key->len > 15) return false;
@@ -2988,6 +3071,40 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
return false;
}
static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, const ScrDyn *receiver) {
for (size_t i = 0; i < recv->v.obj.len; i++) {
ScrDynEntry *entry = &recv->v.obj.entries[i];
if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue;
if (entry->accessor && entry->setter) {
ScrDyn *args[] = {value};
ScrDyn *setter = scr_dyn_retain(entry->setter);
scr_dyn_this_push_dyn(receiver);
ScrDyn *result = scr_dyn_call(setter, args, 1, "setter");
scr_dyn_this_pop();
scr_dyn_release(setter);
scr_dyn_release(result);
return;
}
if (entry->accessor || !entry->writable) {
static const char msg[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return;
}
}
if (recv->non_extensible && !scr_dyn_entry(recv, key)) {
ScrJsonBuf buffer;
scr_jb_init(&buffer);
scr_jb_puts(&buffer, "Cannot add property ");
scr_jb_write(&buffer, key->data, key->len);
scr_jb_puts(&buffer, ", object is not extensible");
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
return;
}
scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
scr_error_sync_cause(recv, key);
return;
}
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (scr_dyn_class_reflection_fence(recv)) return;
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; }
@@ -2999,36 +3116,14 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
return;
}
if (recv->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < recv->v.obj.len; i++) {
ScrDynEntry *entry = &recv->v.obj.entries[i];
if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue;
if (entry->accessor && entry->setter) {
ScrDyn *args[] = {value};
ScrDyn *setter = scr_dyn_retain(entry->setter);
scr_dyn_this_push_dyn(recv);
ScrDyn *result = scr_dyn_call(setter, args, 1, "setter");
scr_dyn_this_pop();
scr_dyn_release(setter);
scr_dyn_release(result);
return;
}
if (entry->accessor || !entry->writable) {
static const char msg[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return;
}
}
if (recv->non_extensible && !scr_dyn_entry(recv, key)) {
ScrJsonBuf buffer;
scr_jb_init(&buffer);
scr_jb_puts(&buffer, "Cannot add property ");
scr_jb_write(&buffer, key->data, key->len);
scr_jb_puts(&buffer, ", object is not extensible");
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
return;
}
scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
scr_error_sync_cause(recv, key);
scr_dyn_object_key_set(recv, key, value, recv);
return;
}
if (recv->kind == SCR_DYN_FUNC) {
if (scr_dyn_fn_property_fence(key)) return;
ScrDyn *table = scr_dyn_fn_properties(recv);
scr_dyn_object_key_set(table, key, value, recv);
scr_dyn_release(table);
return;
}
if (recv->kind == SCR_DYN_ARR) {
@@ -3878,6 +3973,13 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
if (scr_dyn_class_reflection_fence(recv)) return;
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; }
if (recv->kind == SCR_DYN_FUNC) {
if (scr_dyn_fn_property_fence(key)) return;
ScrDyn *table = scr_dyn_fn_properties(recv);
scr_dyn_key_delete(table, key, strict);
scr_dyn_release(table);
return;
}
if (recv->kind == SCR_DYN_OBJ) {
ScrDynEntry *entry = scr_dyn_entry(recv, key);
if (entry && !entry->configurable) {
@@ -4173,9 +4275,18 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) {
if (d->v.fn.clo->props) {
ScrDyn *table = (ScrDyn *)scr_box_get_ref(d->v.fn.clo->props); /* +1 */
ScrDyn *m = table ? scr_dyn_obj_get(table, key, key_len) : NULL;
ScrDyn *r = m ? scr_dyn_retain(m) : NULL;
ScrDyn *r = m ? scr_dyn_obj_read_receiver(table, key, key_len, d) : NULL;
scr_dyn_release(table);
if (r) return r;
if (m || scr_exc_pending()) return r;
/* Deleted own name/length reveal Function.prototype's defaults. */
if (key_len == 4 && memcmp(key, "name", 4) == 0) {
ScrStr *empty = scr_str_new("", 0);
r = scr_dyn_new_str(empty);
scr_str_release(empty);
return r;
}
if (key_len == 6 && memcmp(key, "length", 6) == 0) return scr_dyn_new_num(0);
return NULL;
}
if (key_len == 4 && memcmp(key, "name", 4) == 0) {
const char *n = d->v.fn.name ? d->v.fn.name : "";
@@ -4368,14 +4479,14 @@ bool scr_dyn_err_instanceof(const ScrDyn *d, double kind) {
return false;
}
ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len) {
static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver) {
for (size_t i = 0; i < d->v.obj.len; i++) {
ScrDynEntry *entry = &d->v.obj.entries[i];
if (entry->key_len != key_len || memcmp(entry->key, key, key_len) != 0) continue;
if (!entry->accessor) return scr_dyn_retain(entry->value);
if (!entry->getter) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *getter = scr_dyn_retain(entry->getter);
scr_dyn_this_push_dyn(d);
scr_dyn_this_push_dyn(receiver);
ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter");
scr_dyn_this_pop();
scr_dyn_release(getter);
@@ -4406,6 +4517,10 @@ ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len) {
return scr_dyn_retain(scr_dyn_undefined());
}
ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len) {
return scr_dyn_obj_read_receiver(d, key, key_len, d);
}
/* ── Object.keys/values/entries over the checked-dynamic tree ──────────────────────────
* JS own-key order: array-index keys ascending first, then the rest in
* insertion order. entries answers [key, value] pairs; values RETAIN
@@ -4492,11 +4607,11 @@ static void scr_dyn_objwalk_push(ScrDyn *out, ScrObjWalk mode, const char *key,
}
static bool scr_dyn_objwalk_entry(ScrDyn *out, const ScrDyn *object,
ScrObjWalk mode, const ScrStr *key) {
ScrObjWalk mode, const ScrStr *key, const ScrDyn *receiver) {
if (!scr_dyn_obj_enumerable(object, key->data, key->len)) return true;
ScrDyn *value = NULL;
if (mode != SCR_OBJWALK_KEYS) {
value = scr_dyn_obj_read(object, key->data, key->len);
value = scr_dyn_obj_read_receiver(object, key->data, key->len, receiver);
if (!value) return false;
}
scr_dyn_objwalk_push(out, mode, key->data, key->len, value);
@@ -4597,16 +4712,19 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
return scr_dyn_jsval_ops()->obj_walk(v->v.jsval.cell, (int)mode);
}
ScrDyn *out = scr_dyn_new_arr();
if (v->kind == SCR_DYN_OBJ) {
ScrDyn *keys = scr_dyn_obj_own_keys(v);
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_FUNC) {
ScrDyn *table = v->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(v) : scr_dyn_retain((ScrDyn *)v);
ScrDyn *keys = scr_dyn_obj_own_keys(table);
for (size_t i = 0; i < keys->v.arr.len; i++) {
if (!scr_dyn_objwalk_entry(out, v, mode, keys->v.arr.items[i]->v.str)) {
if (!scr_dyn_objwalk_entry(out, table, mode, keys->v.arr.items[i]->v.str, v)) {
scr_dyn_release(keys);
scr_dyn_release(table);
scr_dyn_release(out);
return NULL;
}
}
scr_dyn_release(keys);
scr_dyn_release(table);
return out;
}
if (v->kind == SCR_DYN_ARR || v->kind == SCR_DYN_BYTES) {
+2 -4
View File
@@ -1029,13 +1029,11 @@ ScrStr *scr_process_cwd(void) {
* — this synchronous runtime has no queued backpressure, so it is constantly
* true. */
bool scr_process_stdout_write(const ScrStr *data) {
scr_stdio_write(1, data->data, data->len);
return true;
return scr_stdio_write(1, data->data, data->len);
}
bool scr_process_stderr_write(const ScrStr *data) {
scr_stdio_write(2, data->data, data->len);
return true;
return scr_stdio_write(2, data->data, data->len);
}
/* The FIRST-CLASS stream write (`output.write(line)` where output is a
+6 -1
View File
@@ -2286,7 +2286,9 @@ ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */
/* Submit one raw chunk to fd 1/2 and flush it before returning. Used by all
* JavaScript-visible console/process/readline/island output paths so the
* internal stdio formatting buffer never delays live output. */
void scr_stdio_write(int fd, const void *data, size_t len);
bool scr_stdio_write(int fd, const void *data, size_t len);
int scr_stdio_write_raw(int fd, const void *data, size_t len);
extern bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len);
/* process.stdout/.stderr .write — raw bytes (no newline or formatting; data
* borrowed), promptly visible and ordered with console output. Constantly
* true (the synchronous runtime never queues backpressure). */
@@ -3377,6 +3379,7 @@ typedef enum {
SCR_DYNH_WEAK_SET, /* native weak membership */
SCR_DYNH_SET, /* native Set<unknown>, shared backing map */
SCR_DYNH_REGEXP, /* native RegExp, shared compiled pattern */
SCR_DYNH_STDIO, /* stable process stdin/stdout/stderr values */
SCR_DYNH_COUNT,
} ScrDynHandleTag;
@@ -3737,6 +3740,7 @@ bool scr_dyn_err_instanceof(const ScrDyn *d, double kind);
ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len);
/* JS typeof comparison, including null's "object" result. */
bool scr_dyn_is_object(const ScrDyn *d);
void scr_dyn_throw(ScrDyn *d); /* moves; preserves boxed native Error classification */
/* structuredClone over the checked-dynamic tree: JSON-safe data + bytes deep-copy;
* functions/handles throw the spec's catchable DataCloneError; cycles
@@ -3874,6 +3878,7 @@ ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known);
ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter);
ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member);
ScrDyn *scr_global_native(ScrArr *known);
ScrDyn *scr_process_stdio(double fd);
void scr_dyn_handle_install(ScrDynHandleTag tag, const ScrDynHandleOps *ops);
void scr_file_handle_dyn_install(void);
+30
View File
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
function throwValue(value) { throw value; }
function same(actual, expected) { assert.strictEqual(actual, expected); }
function relay(value) {
try { throwValue(value); }
catch (error) {
console.log('inner', error instanceof Error, error instanceof TypeError);
throw error;
}
}
const failure = new TypeError('renderer initialization');
function check(value) {
try { relay(value); }
catch (error) {
assert.strictEqual(error, value);
console.log('outer', error instanceof Error, error instanceof TypeError, error instanceof RangeError);
if (error instanceof Error) console.log(error.name, error.message);
}
}
check(failure);
check(new RangeError('bounds'));
check({ name: 'TypeError', message: 'ordinary object', '%error': true });
function reject(value) { return Promise.reject(value); }
try { await reject(failure); }
catch (error) {
same(error, failure);
console.log('rejection', error instanceof Error, error instanceof TypeError);
if (error instanceof Error) console.log(error.message);
}
+19
View File
@@ -0,0 +1,19 @@
function unavailable(value) { throw value; }
function loadBackend(enabled) {
if (enabled) return { open(path, symbols) { return symbols; } };
return { open() { return unavailable(new Error('native backend')); } };
}
var backend = loadBackend(false);
var open = backend.open;
var other = backend['open'];
function getLibrary(value) { return open(value, { symbol: 'entry' }); }
class Library {
load(value) { return other(value, { symbol: 'entry' }); }
}
try { getLibrary('library'); }
catch (error) { console.log(error.message); }
try { new Library().load('library'); }
catch (error) { console.log(error.message); }
backend = loadBackend(true);
open = backend.open;
console.log(getLibrary('library').symbol);
+61
View File
@@ -0,0 +1,61 @@
function render(value) { return value; }
render.options = render.setOptions = function(value) {
this.defaults = value;
return this;
};
render.defaults = { mode: "original" };
const alias = render;
console.log(alias.options === render.setOptions);
console.log(alias.options({ mode: "changed" }) === render, render.defaults.mode);
console.log(render("ok"));
let log = "";
Object.defineProperties(render, {
setting: {
configurable: true, enumerable: true,
get: function() { log += "get;"; return this.defaults.mode; },
set: function(value) { log += "set;"; this.defaults = { mode: value }; }
},
fixed: { value: 7 }
});
console.log(alias.setting = "updated", render.setting);
console.log(log);
console.log(Object.keys(alias).join(","));
const desc = Object.getOwnPropertyDescriptor(alias, "setting");
console.log(typeof desc.get, typeof desc.set, desc.enumerable, desc.configurable);
for (const key of ["fixed", "name", "length"]) {
try { alias[key] = 5; } catch (error) { console.log(key, error.name); }
}
Object.defineProperty(render, "name", { writable: true });
function write(target, key, value) { target[key] = value; }
write(render, "name", "renamed");
console.log(alias.name, render.length);
function receiver() { log += "receiver;"; return alias; }
function key() { log += "key;"; return { toString() { log += "coerce;"; return "choice"; } }; }
function value() { log += "value;"; return 42; }
log = "";
console.log(receiver()[key()] = render.other = value());
console.log(log, alias.choice, alias.other);
function remove(target, key) { delete target[key]; }
remove(alias, "other");
console.log(Object.hasOwn(render, "other"), "choice" in render);
Object.defineProperty(alias, "method", {
get: function() { log += "method;"; return function(arg) { return this === render ? arg : "wrong"; }; }, configurable: true
});
log = "";
console.log(render.method(value()), log);
Object.defineProperty(alias, "fails", {
set: function(value) { log += String(value); throw new TypeError("setter failed"); }
});
log = "";
try { alias.fails = value(); } catch (error) { console.log(error.name, error.message, log); }
Object.defineProperty(alias, "throws", {
get: function() { log += "getter;"; throw new TypeError("getter failed"); }
});
log = "";
try { render.throws(value()); } catch (error) { console.log(error.name, error.message, log); }
remove(alias, "name");
remove(alias, "length");
console.log(JSON.stringify(alias.name), alias.length, Object.hasOwn(alias, "name"), "name" in render);
const data = Object.getOwnPropertyDescriptor(alias, "choice");
console.log(data.value, data.writable, data.enumerable, data.configurable);
+18
View File
@@ -0,0 +1,18 @@
function create(seed) {
const callable = function() { return seed; };
callable.self = callable;
callable.metadata = { seed, callable };
Object.defineProperty(callable, "read", {
get: function() { return callable.metadata.seed; }, configurable: true
});
return callable;
}
const live = create(7);
let total = 0;
for (let i = 0; i < 2000; i++) {
const callable = create(i);
total += callable.read;
if (callable.self !== callable) throw new Error("self");
if (callable.metadata.callable !== callable) throw new Error("nested identity");
}
console.log(total, live(), live.read, live.self === live);
+21
View File
@@ -0,0 +1,21 @@
const input = process.stdin;
const output = process.stdout;
const error = process.stderr;
console.log(input === process.stdin, output === process.stdout, error === process.stderr);
console.log(input === output, output === error, input.fd, output.fd, error.fd);
console.log(Boolean(input.isTTY), Boolean(output.isTTY), output.columns ?? 80, output.rows ?? 24);
console.log(typeof input.setRawMode, typeof input.on, input.readableLength);
console.log(input.pause() === input, input.isPaused());
console.log(input.resume() === input, input.isPaused());
input.pause();
let calls = 0;
const original = output.write;
output.write = function(chunk, encoding, callback) {
calls++;
return original.call(this, chunk, encoding, callback);
};
output.write(Buffer.from('bytes\n'), () => {
output.write = original;
console.log('completed', calls, output.write === original);
});
console.log('through-console');
+14
View File
@@ -0,0 +1,14 @@
import { closeSync } from 'node:fs';
const output = process.stdout;
const error = process.stderr;
function removed() { error.write('removed\n'); }
output.on('error', removed);
output.off('error', removed);
output.once('error', function(err) {
error.write(`event ${this === output} ${err.code}\n`);
});
closeSync(1);
const result = output.write('lost', err => {
error.write(`callback ${err ? err.message : 'none'}\n`);
});
error.write(`returned ${result}\n`);
+18
View File
@@ -0,0 +1,18 @@
const output = process.stdout;
const original = output.write;
const messages = [];
output.write = function(chunk, encoding, callback) {
messages.push(`${this === output}:${typeof chunk}:${String(chunk).trim()}`);
return false;
};
const result = output.write('direct');
console.log('console', -0, true);
output.write = original;
console.log(result, messages.join('|'));
output.write = function() { throw new Error('write failed'); };
console.log('swallowed');
try { output.write('throws'); } catch (error) { messages.push(error.message); }
output.write = original;
console.log(messages[messages.length - 1]);
output.write('6869\n', 'hex');
output.write('\n');
+20
View File
@@ -0,0 +1,20 @@
const input = process.stdin;
const chunks = [];
function removed() { throw new Error('removed listener ran'); }
input.on('data', removed);
input.removeListener('data', removed);
input.on('data', function(chunk) {
chunks.push(String(chunk));
console.log('receiver', this === input);
input.pause();
setTimeout(() => input.resume(), 20);
});
input.on('end', function() {
console.log('end', this === input, chunks.join(''));
});
input.pause();
console.log('paused', input.isPaused());
setTimeout(() => {
console.log('resume');
input.resume();
}, 30);
+4
View File
@@ -0,0 +1,4 @@
const input = process.stdin;
input.on('data', () => { throw new Error('paused data ran'); });
input.pause();
console.log('exit', input.isPaused());
+14
View File
@@ -0,0 +1,14 @@
const input = process.stdin;
input.pause();
console.log('initial', input.read() === null);
let text = '';
let polls = 0;
const timer = setInterval(() => {
let chunk;
while ((chunk = input.read()) !== null) text += String(chunk);
if (text === 'hello' || ++polls > 100) {
clearInterval(timer);
input.pause();
console.log('read', text, input.readableLength);
}
}, 10);
+15
View File
@@ -19,6 +19,21 @@ export interface EventLoopCase {
}
export const eventLoopCases: EventLoopCase[] = [
{
title: "stdio values: paused reads consume buffered input",
fixture: "stdio-read.mjs",
script: { writes: [{ delayMs: 30, data: "hello" }], end: true },
},
{
title: "stdio values: pause preserves data until resume",
fixture: "stdio-pause-resume.mjs",
script: { writes: [{ delayMs: 0, data: "hello" }], end: true },
},
{
title: "stdio values: paused input releases the event loop",
fixture: "stdio-paused-exit.mjs",
script: { writes: [], end: false },
},
{
title: "read-all: one instant write, then EOF",
fixture: "stdin-read-all.ts",