Preserve native class state and handle terminal process events

- Preserve fields, accessors, added properties, and cyclic references across untyped class boundaries.
- Support platform signal names and native exception handlers that resume queued work.
This commit is contained in:
Chris Tate
2026-09-29 09:07:31 -05:00
parent f8906d0b75
commit e24c8a14dc
42 changed files with 1121 additions and 289 deletions
+7
View File
@@ -13,6 +13,7 @@ These are rejected at compile time with an `SC` code, a code frame, and usually
**Language edges**
- `Error`, `TypeError`, `RangeError`, and `SyntaxError` accept `{ cause }` options, including through inherited constructors. Reading, assigning, deleting, and testing `"cause" in error` preserve the distinction between an absent cause and an explicitly undefined cause. Constructor causes are non-enumerable; assignment creates an enumerable property when the cause is absent. Property descriptors and checked-value aliases share the same cause, including getters and setters installed after construction. Accessor constructor options and explicit subclass redeclarations of `cause` remain unsupported.
- Native class fields, inherited accessors, and added properties retain their identity across untyped values. Reflection such as `JSON.stringify` and `Object.keys` still requires every declared field to have a supported checked-value representation; classes containing opaque fields such as native Maps retain a checked error for those operations.
- Native class methods can be stored and passed as unbound function values. A field call supplies the containing instance as `this`; `call`, `apply`, and `bind` work when the function signature can cross the checked-value boundary. Extracted overrides keep the selected method even when called with another receiver. Receiver validation accepts the declaring class and its subclasses; borrowing a method onto an unrelated object remains unsupported. Detached methods that use `this`, builtin method values, abstract method declarations, unspecialized generic methods, and incompatible override signatures retain checked errors or compile-time refusals.
- Loose `==`/`!=` comparisons compile across statically represented primitive and primitive-union operands, including number/string/boolean/BigInt coercions and the `x == null`/`x != null` nullish idiom. Object-to-primitive comparisons remain fenced because custom `valueOf`/`toString` methods can execute arbitrary code; convert the object explicitly first.
- Block- and function-scoped `using` and `await using` compile for statically represented classes with zero-parameter disposal methods and for the supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops compile. Top-level and switch-clause declarations, disposal through an engine-held package value, and the `DisposableStack` constructors remain fenced; a caught `SuppressedError` exposes its Node-compatible `name` and `message`, while its `error` and `suppressed` payload properties remain outside the static catch surface.
@@ -120,6 +121,12 @@ A static-tier program otherwise produces byte-identical stdout and the same exit
- **Top-level `await` in embedded ESM packages** is not supported yet. It does compile in your program's own ESM graph and in npm packages compiled through `--npm-static`; the remaining limit is package code running inside the `--dynamic` island.
- **`--npm-static` and `--provenance-sources` are experimental** — see [npm Dependencies](/dependencies) for the maturity notes.
## Process events
Static signal listeners resolve names on the target platform, including `SIGWINCH` for terminal resize on POSIX. `on`, `once`, `off`, and `removeListener` accept computed signal names and deliver the signal name and number. Computed names for other process-event families and chained registrations remain unsupported. Windows signal registration remains limited to the CRT signal set; WASI has no OS signals.
Literal `uncaughtException` and `uncaughtExceptionMonitor` registrations compile without a JavaScript engine. Handled exceptions resume queued native work; monitors alone do not suppress failure. Error objects and primitive exception values cross the listener boundary, while arbitrary native objects retain the existing exception-value restrictions. Exception capture callbacks and dynamic-island exception handling remain unsupported.
## Native addons
Node-API (N-API) and V8 `.node` addons require Node's addon runtime, which scriptc does not embed in either static or `--dynamic` builds. Direct `createRequire` calls to local addons are rejected at compile time. Embedded npm packages receive a catchable `ERR_DLOPEN_FAILED` when they attempt to load an addon, allowing packages with JavaScript fallbacks to select them.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "a063593865cc30056a9c",
"artifactVersion": "e6aa3e9df87eb18c506b",
"rowCount": 3662
}
+11 -11
View File
@@ -842,9 +842,9 @@
"apiEntries": 119,
"static": {
"supported": 0,
"partial": 30,
"partial": 33,
"refused": 0,
"not-implemented": 89,
"not-implemented": 86,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -65109,9 +65109,9 @@
"anchor": "event-uncaughtexception",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -65155,9 +65155,9 @@
"anchor": "event-uncaughtexceptionmonitor",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -65247,9 +65247,9 @@
"anchor": "signal-events",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 47,
"verify-gap": 2565,
"audit-partial": 600,
"verify-gap": 2562,
"audit-partial": 603,
"classify": 2,
"implement": 81
},
@@ -70558,12 +70558,19 @@
"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.'uncaughtException'",
"tests": [
"tests/corpus/process-uncaught-sync.cjs",
"tests/corpus/process-uncaught-loop.cjs",
"tests/corpus/process-uncaught-module.mjs",
"tests/corpus/process-uncaught-rejection.cjs",
"tests/corpus/process-uncaught-monitor.cjs",
"tests/corpus/process-uncaught-handler-throws.cjs"
]
},
"dynamic": {
"status": "not-implemented",
@@ -70614,12 +70621,19 @@
"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.'uncaughtExceptionMonitor'",
"tests": [
"tests/corpus/process-uncaught-sync.cjs",
"tests/corpus/process-uncaught-loop.cjs",
"tests/corpus/process-uncaught-module.mjs",
"tests/corpus/process-uncaught-rejection.cjs",
"tests/corpus/process-uncaught-monitor.cjs",
"tests/corpus/process-uncaught-handler-throws.cjs"
]
},
"dynamic": {
"status": "not-implemented",
@@ -70726,12 +70740,15 @@
"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.SIGINT, SIGHUP, etc.",
"tests": [
"tests/corpus/1443-signal-handlers.ts",
"tests/corpus/process-named-signals.cjs"
]
},
"dynamic": {
"status": "not-implemented",
@@ -915,9 +915,9 @@
"apiEntries": 119,
"static": {
"supported": 0,
"partial": 30,
"partial": 33,
"refused": 0,
"not-implemented": 89,
"not-implemented": 86,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -86848,6 +86848,7 @@
"evidence": "derived:descendants",
"tests": [
"packages/compiler/test/process-builtin-values.test.ts",
"tests/corpus/1443-signal-handlers.ts",
"tests/corpus/2314-process-introspection.ts",
"tests/corpus/2941-process-exit-code.ts",
"tests/corpus/2942-process-exit-code-override.ts",
@@ -86856,6 +86857,13 @@
"tests/corpus/2946-child-fork-ipc/main.ts",
"tests/corpus/990-process-basics.ts",
"tests/corpus/process-builtin-values.js",
"tests/corpus/process-named-signals.cjs",
"tests/corpus/process-uncaught-handler-throws.cjs",
"tests/corpus/process-uncaught-loop.cjs",
"tests/corpus/process-uncaught-module.mjs",
"tests/corpus/process-uncaught-monitor.cjs",
"tests/corpus/process-uncaught-rejection.cjs",
"tests/corpus/process-uncaught-sync.cjs",
"tests/corpus/process-versions-storage.ts",
"tests/corpus/stdio-values.mjs",
"tests/corpus/stdio-write-errors.mjs",
@@ -87072,8 +87080,16 @@
"anchor": "event-uncaughtexception",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "compiler-feature:process.'uncaughtException'",
"tests": [
"tests/corpus/process-uncaught-sync.cjs",
"tests/corpus/process-uncaught-loop.cjs",
"tests/corpus/process-uncaught-module.mjs",
"tests/corpus/process-uncaught-rejection.cjs",
"tests/corpus/process-uncaught-monitor.cjs",
"tests/corpus/process-uncaught-handler-throws.cjs"
]
},
"dynamic": {
"status": "not-implemented",
@@ -87116,8 +87132,16 @@
"anchor": "event-uncaughtexceptionmonitor",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "compiler-feature:process.'uncaughtExceptionMonitor'",
"tests": [
"tests/corpus/process-uncaught-sync.cjs",
"tests/corpus/process-uncaught-loop.cjs",
"tests/corpus/process-uncaught-module.mjs",
"tests/corpus/process-uncaught-rejection.cjs",
"tests/corpus/process-uncaught-monitor.cjs",
"tests/corpus/process-uncaught-handler-throws.cjs"
]
},
"dynamic": {
"status": "not-implemented",
@@ -87248,8 +87272,12 @@
"anchor": "signal-events",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "compiler-feature:process.SIGINT, SIGHUP, etc.",
"tests": [
"tests/corpus/1443-signal-handlers.ts",
"tests/corpus/process-named-signals.cjs"
]
},
"dynamic": {
"status": "not-implemented",
@@ -88,6 +88,8 @@
},
"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": ["Signal events"], "status": "partial", "evidence": ["tests/corpus/1443-signal-handlers.ts", "tests/corpus/process-named-signals.cjs"] },
{ "chapter": "process", "signatures": ["Event: `'uncaughtException'`", "Event: `'uncaughtExceptionMonitor'`"], "status": "partial", "evidence": ["tests/corpus/process-uncaught-sync.cjs", "tests/corpus/process-uncaught-loop.cjs", "tests/corpus/process-uncaught-module.mjs", "tests/corpus/process-uncaught-rejection.cjs", "tests/corpus/process-uncaught-monitor.cjs", "tests/corpus/process-uncaught-handler-throws.cjs"] },
{ "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"] },
+7 -7
View File
@@ -847,11 +847,11 @@ export class CEmitter {
: runExitListeners !== ""
? ` ${runExitListeners.trim()}`
: ` /* no refcounted globals */`;
const uncaught = (indent: string, releaseTop = false) => [
`${indent}if (scr_exc_pending()) {`,
const uncaught = (indent: string, releaseTop = false, handle = false) => [
`${indent}if (scr_exc_pending()${handle ? " && !scr_exc_handle_uncaught(false)" : ""}) {`,
`${indent} scr_exc_print_uncaught();`,
`${indent} ${releaseTop ? "scr_promise_release(sc_top); " : ""}` +
`${exitCleanup}return 1;`,
`${exitCleanup}return scr_exit_code_hint_get();`,
`${indent}}`,
];
out.push(
@@ -947,7 +947,7 @@ export class CEmitter {
? [` ScrPromise *sc_top = ${mangleAsyncSpawn(this.mod.entry)}();`]
: [` ${mangleFunction(this.mod.entry)}();`]),
// Uncaught exception from top-level code: Node exits 1.
...(this.mayThrow.has(this.mod.entry) && !asyncEntry ? uncaught(" ") : []),
...(this.mayThrow.has(this.mod.entry) && !asyncEntry ? uncaught(" ", false, true) : []),
// The event loop runs to exhaustion (microtasks before timers). A
// throw escaping a timer callback and unhandled promise rejections
// both exit 1, like Node.
@@ -958,7 +958,7 @@ export class CEmitter {
` if (sc_loop_rejection) {`,
` scr_discard_unhandled_rejections();`,
...(asyncEntry ? [` scr_promise_release(sc_top);`] : []),
` ${exitCleanup}return 1;`,
` ${exitCleanup}return scr_exit_code_hint_get();`,
` }`,
...(asyncEntry
? [
@@ -971,13 +971,13 @@ export class CEmitter {
` scr_promise_rethrow_top_level(sc_top);`,
` scr_promise_release(sc_top);`,
` scr_exc_print_uncaught();`,
` ${exitCleanup}return 1;`,
` ${exitCleanup}return scr_exit_code_hint_get();`,
` }`,
` scr_promise_release(sc_top);`,
]
: []),
` if (scr_report_unhandled_rejections()) {`,
` ${exitCleanup}return 1;`,
` ${exitCleanup}return scr_exit_code_hint_get();`,
` }`,
...(asyncEntry
? [
+33 -15
View File
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
* expression lands in a fresh C temp, with RC ownership tracked on the
* emitter's frames (see the discipline comment in emitter core). */
import type { CEmitter, Temp } from "./c-emitter.js";
import { BYTES_ELEMENT_SIZE, arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
import { BYTES_ELEMENT_SIZE, arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, DYN_CLASS_PROPERTIES, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
import { OVERFLOW_MEMBER } from "./shapes.js";
@@ -110,6 +110,22 @@ function streamTypedRefCommitAdapter(
` }`,
);
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
const bag = `sc_target->${mangleField(DYN_CLASS_PROPERTIES)}`;
defs.push(
` ScrDyn *sc_bag = scr_dyn_new_obj();`,
` scr_dyn_release(scr_dyn_copy_data_properties(sc_bag, sc_d));`,
` if (scr_exc_pending()) { scr_dyn_release(sc_bag); return; }`,
);
for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) {
const literal = cStringLiteral(Buffer.from(field.name, "utf8"));
defs.push(
` { ScrStr *sc_key = scr_str_new(${literal}, ${Buffer.byteLength(field.name, "utf8")});`,
` scr_dyn_key_delete(sc_bag, sc_key, false); scr_str_release(sc_key); }`,
);
}
defs.push(` ScrDyn *sc_old_bag = ${bag};`, ` ${bag} = sc_bag;`, ` scr_dyn_release(sc_old_bag);`);
}
defs.push(`}`, ``);
return `&${commit}`;
}
@@ -194,7 +210,7 @@ function nestedTypedRefUnionAdapter(
const armKey = typeKey(arm);
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
lines.push(
` case ${tag}: return scr_dyn_new_typed_ref(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
` case ${tag}: return scr_dyn_new_typed_ref${emitter.traceAdapterC(arm) !== null ? "_traced" : ""}(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
);
} else if (arm.kind === "undefinedT") {
lines.push(` case ${tag}: return scr_dyn_retain(scr_dyn_undefined());`);
@@ -265,7 +281,7 @@ function streamTypedRefAdapter(
const rc = vAdapters(child);
const childKey = typeKey(child);
const keyLit = cStringLiteral(Buffer.from(childKey, "utf8"));
return `scr_dyn_new_typed_ref(${expr}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(childKey, "utf8")}, &${nested.snapshot}, ${nested.commit})`;
return `scr_dyn_new_typed_ref${emitter.traceAdapterC(child) !== null ? "_traced" : ""}(${expr}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(childKey, "utf8")}, &${nested.snapshot}, ${nested.commit})`;
};
const lines = [
@@ -284,6 +300,9 @@ function streamTypedRefAdapter(
` scr_dyn_obj_set(d, ${keyLit}, ${Buffer.byteLength(field.name, "utf8")}, ${box(field.type, `v->${mangleField(field.name)}`)});`,
);
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
lines.push(` scr_dyn_release(scr_dyn_copy_data_properties(d, v->${mangleField(DYN_CLASS_PROPERTIES)}));`);
}
lines.push(` return d;`);
} else if (t.kind === "record") {
const shape = emitter.recordsById.get(t.shapeId);
@@ -431,7 +450,7 @@ function liveDynUnionRefAdapter(
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
defs.push(
` case ${tag}:`,
` return scr_dyn_new_typed_ref(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
` return scr_dyn_new_typed_ref${emitter.traceAdapterC(arm) !== null ? "_traced" : ""}(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
);
}
const mutableTags = new Set(mutableArms.map(({ tag }) => tag));
@@ -546,7 +565,7 @@ function streamFromArrayAdapter(
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
d.push(
` case ${tag}:`,
` sc_d = scr_dyn_new_typed_ref(scr_union_peek(sc_v), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${snapshot}_${tag}, ${unionCommits.get(tag) ?? "NULL"});`,
` sc_d = scr_dyn_new_typed_ref${emitter.traceAdapterC(arm) !== null ? "_traced" : ""}(scr_union_peek(sc_v), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${snapshot}_${tag}, ${unionCommits.get(tag) ?? "NULL"});`,
` break;`,
);
}
@@ -561,7 +580,7 @@ function streamFromArrayAdapter(
const keyLit = cStringLiteral(Buffer.from(key, "utf8"));
const keyLen = Buffer.byteLength(key, "utf8");
d.push(
` ScrDyn *sc_d = scr_dyn_new_typed_ref(sc_v, &${rc.retain}, &${rc.release}, ${keyLit}, ${keyLen}, &${snapshot}, ${commit});`,
` ScrDyn *sc_d = scr_dyn_new_typed_ref${emitter.traceAdapterC(elem) !== null ? "_traced" : ""}(sc_v, &${rc.retain}, &${rc.release}, ${keyLit}, ${keyLen}, &${snapshot}, ${commit});`,
);
} else {
d.push(` ScrDyn *sc_d = ${emitter.toDynHelper(elem)}(sc_v);`);
@@ -2933,7 +2952,7 @@ function emitDynamicExpr(
const keyLit = cStringLiteral(Buffer.from(key, "utf8"));
return emitter.newTemp(
e.type,
`scr_dyn_new_typed_ref(${v.name}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit})`,
`scr_dyn_new_typed_ref${emitter.traceAdapterC(v.type) !== null ? "_traced" : ""}(${v.name}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit})`,
);
}
if (v.type.kind === "func") {
@@ -8071,6 +8090,11 @@ function emitProcessLibCall(state: LibCallState): Temp {
return finish(`scr_process_off_warning(${arg(0)})`);
case "process.emitWarning":
return finish(`scr_process_emit_warning(${arg(0)})`);
case "process.onUncaughtException":
emitter.usesTimers = true;
return finish(`scr_process_on_uncaught_exception(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "process.offUncaughtException":
return finish(`scr_process_off_uncaught_exception(${arg(0)}, ${arg(1)})`);
case "process.onUnhandledRejection":
// The completed-checkpoint report dispatches the listeners.
emitter.usesTimers = true;
@@ -8276,16 +8300,10 @@ function emitProcessLibCall(state: LibCallState): Temp {
emitter.moveTemp(cb);
return finish(`scr_next_tick(${cb.name})`);
}
case "process.onSignal": {
// The registry owns the callback (zero-param — frontend-pinned)
// until off/once removes it. The loop dispatches deliveries.
case "process.onSignal":
emitter.usesTimers = true;
const cb = args[1]!;
emitter.moveTemp(cb);
return finish(`scr_signal_on(${arg(0)}, ${cb.name}, ${arg(2)})`);
}
return finish(`scr_signal_on(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "process.offSignal":
// Borrowed callback: removal is by pointer identity.
return finish(`scr_signal_off(${arg(0)}, ${arg(1)})`);
case "process.onExit": {
// Runtime adapters cover both shapes (the code is a plain
+1 -1
View File
@@ -1595,7 +1595,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
const key = typeKey(t);
const keyLit = cStringLiteral(Buffer.from(key, "utf8"));
d.push(
` return scr_dyn_new_typed_ref(v, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
` return scr_dyn_new_typed_ref${emitter.traceAdapterC(t) !== null ? "_traced" : ""}(v, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
);
}
break;
+3 -4
View File
@@ -1,3 +1,4 @@
import { typedRefConstructor } from "./shapes.js";
import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
import { InternalCompilerError } from "../../errors.js";
/* The dyn (ScrDyn dyn) helper EMITTERS for the LLVM backend — the .ll
@@ -1423,12 +1424,10 @@ export class LlDyn {
const adapter = host.liveDynRefAdapter(t);
const rc = vAdapters(host, t);
const keyLit = host.cstr(typeKey(t));
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const r = B.tmp();
B.line(
`${r} = call ptr @scr_dyn_new_typed_ref(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyLit}, ${host.sizeType} ${Buffer.byteLength(typeKey(t), "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
`${r} = call ptr ${typedRefConstructor(host, t)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyLit}, ${host.sizeType} ${Buffer.byteLength(typeKey(t), "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
);
B.terminate(`ret ptr ${r}`);
break;
+15 -7
View File
@@ -988,7 +988,7 @@ export class LlEmitter {
// unchanged.
const hasRefGlobals = globals.some((g) => isRefCounted(g.type)) || fnValueProps.length > 0;
const inlineExitListeners = usesEvents && (hasRefGlobals || this.ffiHasRetainedCallback);
if (!usesNodeTest && !usesIsland) this.declare(`declare i32 @scr_exit_code_hint_get()`);
this.declare(`declare i32 @scr_exit_code_hint_get()`);
if (inlineExitListeners) {
this.declare(`declare void @scr_run_exit_listeners(double)`);
this.declare(`declare i32 @scr_exit_code_hint_get()`);
@@ -1394,7 +1394,7 @@ export class LlEmitter {
// order-free — the scr_error_set_traced precedent). Only the
// printer emits here (nothing else declares it); scr_exc_pending
// and the loop entry points rode the Set before the flush.
out.push(`declare void @scr_exc_print_uncaught()`, ``);
out.push(`declare void @scr_exc_print_uncaught()`, `declare zeroext i1 @scr_exc_handle_uncaught(i1 zeroext)`, ``);
}
if (this.mod.lib !== undefined) {
// LIBRARY mode: no @main — the profile-declared external
@@ -1448,10 +1448,14 @@ export class LlEmitter {
` %exc = call zeroext i1 @scr_exc_pending()`,
` br i1 %exc, label %uncaught, label %ok`,
`uncaught:`,
` %handled = call zeroext i1 @scr_exc_handle_uncaught(i1 false)`,
` br i1 %handled, label %ok, label %fatal`,
`fatal:`,
` call void @scr_exc_print_uncaught()`,
...exitListenerLines("xu"),
...uncaughtReleases,
` ret i32 1`,
` %failure_code_0 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_0`,
`ok:`,
]
: []),
@@ -1468,7 +1472,8 @@ export class LlEmitter {
...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []),
...exitListenerLines("xl"),
...loopReleasesU,
` ret i32 1`,
` %failure_code_1 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_1`,
`lok:`,
` br i1 %loop_rejection, label %lreported, label %lclean`,
`lreported:`,
@@ -1476,7 +1481,8 @@ export class LlEmitter {
...(asyncEntry ? [` call void @scr_promise_release(ptr %top)`] : []),
...exitListenerLines("xq"),
...loopReportedReleases,
` ret i32 1`,
` %failure_code_2 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_2`,
`lclean:`,
...(asyncEntry
? [
@@ -1493,7 +1499,8 @@ export class LlEmitter {
` call void @scr_exc_print_uncaught()`,
...exitListenerLines("xt"),
...topRejectReleases,
` ret i32 1`,
` %failure_code_3 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_3`,
`tla_not_rejected:`,
` call void @scr_promise_release(ptr %top)`,
]
@@ -1503,7 +1510,8 @@ export class LlEmitter {
`lrej:`,
...exitListenerLines("xr"),
...loopReleasesR,
` ret i32 1`,
` %failure_code_4 = call i32 @scr_exit_code_hint_get()`,
` ret i32 %failure_code_4`,
`lrok:`,
...(asyncEntry
? [
@@ -1,3 +1,4 @@
import { typedRefConstructor } from "./shapes.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
@@ -58,12 +59,10 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
host.liveDynRefAdapters.set(key, adapter);
}
const rc = vAdapters(host.shapeHost, v.type);
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const boxed = B.tmp();
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
`${boxed} = call ptr ${typedRefConstructor(host, v.type)}(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
);
return host.own({ name: boxed, type: e.type });
}
@@ -1,7 +1,8 @@
import { typedRefConstructor } from "./shapes.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { type IrType, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { type IrType, DYN_CLASS_PROPERTIES, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { classFieldIndex, classStructSym } from "./classes.js";
@@ -185,6 +186,34 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
}
lines.push(` br label %${after}`, `${after}:`);
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
host.declare(`declare ptr @scr_dyn_new_obj()`);
host.declare(`declare ptr @scr_dyn_copy_data_properties(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
host.declare(`declare void @scr_str_release(ptr)`);
host.declare(`declare void @scr_dyn_key_delete(ptr, ptr, i1 zeroext)`);
lines.push(
` %bag = call ptr @scr_dyn_new_obj()`,
` %bag_copy = call ptr @scr_dyn_copy_data_properties(ptr %bag, ptr %d)`,
` call void @scr_dyn_release_v(ptr %bag_copy)`,
` %bag_pending = call zeroext i1 @scr_exc_pending()`,
` br i1 %bag_pending, label %bag_fail, label %bag_keys`,
`bag_fail:`, ` call void @scr_dyn_release_v(ptr %bag)`, ` br label %done`, `bag_keys:`,
);
fields.forEach((field, index) => lines.push(
` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`,
` call void @scr_str_release(ptr %bag_key${index})`,
));
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
lines.push(
` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`,
` %bag_old = load ptr, ptr %bag_ptr`,
` store ptr %bag, ptr %bag_ptr`,
` call void @scr_dyn_release_v(ptr %bag_old)`,
);
}
lines.push(` br label %done`, `done:`, ` ret void`, `}`, ``);
host.resolveThunkDefs.push(...lines);
return `@${commit}`;
@@ -263,9 +292,7 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
const sym = `sc_ldu_${host.liveDynUnionRefAdapters.size}`;
host.liveDynUnionRefAdapters.set(key, sym);
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const adapters = new Map<number, LlStreamTypedRefAdapter>();
for (const { arm, tag } of mutableArms) {
const prefix = `${sym}_${tag}`;
@@ -312,7 +339,7 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
);
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
`${boxed} = call ptr ${typedRefConstructor(host, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
);
B.terminate(`ret ptr ${boxed}`);
});
@@ -388,9 +415,7 @@ function nestedTypedRefUnionAdapter(
}
const sym = `${ctx.prefix}_union_${unions.size}`;
unions.set(key, sym);
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const B = new BlockBuilder();
const tagPtr = B.tmp();
const tagValue = B.tmp();
@@ -413,7 +438,7 @@ function nestedTypedRefUnionAdapter(
B.line(`${payloadPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 5`);
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
`${boxed} = call ptr ${typedRefConstructor(host, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
);
B.terminate(`ret ptr ${boxed}`);
} else if (arm.kind === "undefinedT") {
@@ -498,11 +523,9 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext,
const nested = host.streamTypedRefMaterializeAdapter(t, ctx);
const rc = vAdapters(host.shapeHost, t);
const key = typeKey(t);
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${nested.snapshot}, ptr ${nested.commit})`,
`${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${nested.snapshot}, ptr ${nested.commit})`,
);
return boxed;
}
@@ -555,6 +578,16 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue, ctx);
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
host.declare(`declare ptr @scr_dyn_copy_data_properties(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
const slot = B.tmp(), bag = B.tmp(), copied = B.tmp();
B.line(`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`);
B.line(`${bag} = load ptr, ptr ${slot}`);
B.line(`${copied} = call ptr @scr_dyn_copy_data_properties(ptr ${out}, ptr ${bag})`);
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
}
B.terminate(`ret ptr ${out}`);
} else if (t.kind === "record") {
const shape = host.recordsById.get(t.shapeId);
@@ -735,9 +768,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
`${boxed} = call ptr @${host.dyn.toDynHelper(elem)}(ptr ${value})`,
);
} else {
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const boxedSlot = B.slot();
B.entryAllocas.push(`${boxedSlot} = alloca ptr`);
const tagPtr = B.tmp();
@@ -769,7 +800,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
);
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
B.line(
`${armBoxed} = call ptr @scr_dyn_new_typed_ref(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${armSnapshot}, ptr ${armCommit})`,
`${armBoxed} = call ptr ${typedRefConstructor(host, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${armSnapshot}, ptr ${armCommit})`,
);
B.line(`store ptr ${armBoxed}, ptr ${boxedSlot}`);
B.br(join);
@@ -815,11 +846,9 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
``,
);
}
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyPtr}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr ${commit})`,
`${boxed} = call ptr ${typedRefConstructor(host, elem)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyPtr}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr ${commit})`,
);
} else {
boxed = B.tmp();
@@ -436,15 +436,13 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
return { name: "", type: e.type };
}
if (e.fn === "process.onSignal") {
// The registry owns the callback (zero-param — frontend-pinned)
// until off/once removes it. The loop dispatches deliveries.
host.usesTimers = true;
const sig = host.emitExpr(e.args[0]!);
const cb = host.emitExpr(e.args[1]!);
const once = host.emitExpr(e.args[2]!);
host.moveTemp(cb);
host.declare(`declare void @scr_signal_on(double, ptr, i1 zeroext)`);
B.line(`call void @scr_signal_on(double ${sig.name}, ptr ${cb.name}, i1 ${once.name})`);
host.declare(`declare void @scr_signal_on(ptr, ptr, i1 zeroext)`);
B.line(`call void @scr_signal_on(ptr ${sig.name}, ptr ${cb.name}, i1 ${once.name})`);
host.emitPendingCheck();
return { name: "", type: e.type };
}
if (e.fn === "process.onExit") {
@@ -849,6 +849,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.onWarning": "scr_process_on_warning",
"process.offWarning": "scr_process_off_warning",
"process.emitWarning": "scr_process_emit_warning",
"process.onUncaughtException": "scr_process_on_uncaught_exception",
"process.offUncaughtException": "scr_process_off_uncaught_exception",
"process.onUnhandledRejection": "scr_process_on_unhandled_rejection",
"process.offUnhandledRejection": "scr_process_off_unhandled_rejection",
"process.onRejectionHandled": "scr_process_on_rejection_handled",
@@ -944,6 +946,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
// the checkpoint unhandled-rejection report.
"async.hop", "async.awaitDyn",
"dc.tcTracePromise",
"process.onUncaughtException",
"process.onUnhandledRejection", "process.onRejectionHandled",
"timers.queueMicrotaskDyn", "timers.setImmediateFnValue", "timers.immediatePromise",
]);
@@ -176,6 +176,13 @@ export function traceArg(host: ShapeHost, t: IrType): string {
return traceAdapter(host, t) ?? "null";
}
/** Keep native capsule edges visible when the referent is cycle-capable. */
export function typedRefConstructor(host: ShapeHost, t: IrType): string {
const name = traceAdapter(host, t) === null ? "scr_dyn_new_typed_ref" : "scr_dyn_new_typed_ref_traced";
host.declare(`declare ptr @${name}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`);
return `@${name}`;
}
/* ── arrays ───────────────────────────────────────────────────────────── */
/** Runtime accessor suffix for an element type (matches types.ts:
@@ -1,9 +1,10 @@
import { BOOL, DYN, STRING, isDynTypedRefType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { varRef } from "../../ir/build.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { dynUndefinedExpr, PoisonError, type Lowerer } from "./lowerer.js";
import { implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
import { findGenericMethodOn, findMethodOn, upcastTo, type ClassInfo } from "./lower-classes.js";
import { accessorCall, findGenericMethodOn, findMethodOn, upcastTo, type ClassInfo } from "./lower-classes.js";
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
interface Dispatch {
@@ -12,6 +13,13 @@ interface Dispatch {
classes: Set<string>;
}
interface PropertyDispatch {
name: string;
write: boolean;
fn: IrFunction;
classes: Set<string>;
}
/** Calls on native class capsules keep the instance's compiled methods. The
* reachable-body fixed point discovers both the boxed classes and method
* names before generating checked native dispatch; ordinary dyn receivers
@@ -19,6 +27,10 @@ interface Dispatch {
export class ClassDynamicDispatch {
private readonly boxed = new Set<string>();
private readonly dispatches = new Map<string, Dispatch>();
private readonly properties = new Map<string, PropertyDispatch>();
private readonly computed = new Map<string, Omit<PropertyDispatch, "name"> & { keyLocal: string; branchIndex: number }>();
private propertyBag: IrFunction | null = null;
private readonly bagClasses = new Set<string>();
private readonly generated = new Set<IrFunction>();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
@@ -46,6 +58,39 @@ export class ClassDynamicDispatch {
});
if (this.boxed.size === 0) return false;
let changed = false;
if (!this.propertyBag) {
const loc = functions[0]!.loc;
this.propertyBag = {
name: "%dyn.class.properties", params: [{ localId: "p.0", name: "value", type: DYN }], returnType: DYN,
locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
body: [{ kind: "return", value: varRef("p.0", DYN, loc), loc }], loc,
};
this.generated.add(this.propertyBag);
lowerer.liftedFns.push(this.propertyBag);
changed = true;
}
for (const className of this.boxed) {
if (this.bagClasses.has(className)) continue;
const info = lowerer.classes.get(className);
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
this.bagClasses.add(className);
this.ensurePropertyBag(info);
const loc = this.propertyBag.loc;
const type: IrType = { kind: "object", className };
const value = varRef("p.0", DYN, loc);
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
const bag: IrExpr = { kind: "fieldGet", obj: receiver, className, field: PROPERTY_BAG, type: DYN, loc };
this.propertyBag.body.unshift({
kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc },
then: [
{ kind: "if", cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc }, then: [
{ kind: "fieldSet", obj: receiver, className, field: PROPERTY_BAG, value: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc },
], else_: null, loc },
{ kind: "return", value: bag, loc },
], else_: null, loc,
});
changed = true;
}
const byMethod = new Map<string, ClassInfo[]>();
const candidates = (method: string): ClassInfo[] => {
const found = byMethod.get(method);
@@ -63,6 +108,74 @@ export class ClassDynamicDispatch {
fn.body = transformStmtList(fn.body, {
stmt: (stmt) => stmt,
expr: (expr) => {
const computedRead = expr.kind === "dynKeyGet" && expr.key.kind !== "strLit" ? expr : null;
const computedWrite = expr.kind === "libCall" && (expr.fn === "dyn.keySetComputed" || expr.fn === "dyn.keySet" && expr.args[1]?.kind !== "strLit") ? expr : null;
if (computedRead || computedWrite) {
const dynamicKey = computedWrite?.fn === "dyn.keySetComputed";
const key = JSON.stringify([!!computedWrite, computedRead?.optional ?? false, dynamicKey]);
let dispatch = this.computed.get(key);
if (!dispatch) {
const loc = expr.loc;
const params = [
{ localId: "p.0", name: "value", type: DYN },
{ localId: "p.key", name: "key", type: dynamicKey ? DYN : STRING },
...(computedWrite ? [{ localId: "p.1", name: "stored", type: DYN }] : []),
];
const keyLocal = dynamicKey ? "key.string" : "p.key";
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, args: [varRef("p.0", DYN, loc)], type: DYN, loc };
const fallback: IrExpr = computedRead
? { ...computedRead, value: bag, key: varRef("p.key", STRING, loc) }
: { ...computedWrite!, fn: "dyn.keySet", args: [bag, varRef(keyLocal, STRING, loc), varRef("p.1", DYN, loc)] };
const helper: IrFunction = {
name: `%dyn.class.computed.${this.computed.size}`, params, returnType: computedWrite ? VOID : DYN,
locals: [...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), ...(dynamicKey ? [{ id: keyLocal, name: "key", type: STRING, mutable: false }] : [])],
body: computedWrite
? [{ kind: "exprStmt", expr: fallback, loc }, { kind: "return", value: null, loc }]
: [{ kind: "return", value: fallback, loc }], loc,
};
if (dynamicKey) helper.body.unshift(
{ kind: "if", cond: { kind: "dynTest", test: "nullish", value: varRef("p.0", DYN, loc), type: BOOL, loc }, then: [
{ kind: "exprStmt", expr: { ...computedWrite!, args: [varRef("p.0", DYN, loc), varRef("p.key", DYN, loc), varRef("p.1", DYN, loc)] }, loc },
{ kind: "return", value: null, loc },
], else_: null, loc },
{ kind: "varDecl", localId: keyLocal, init: { kind: "libCall", fn: "dyn.toStringCoerce", args: [varRef("p.key", DYN, loc)], type: STRING, loc }, loc },
);
dispatch = { write: !!computedWrite, fn: helper, classes: new Set(), keyLocal, branchIndex: dynamicKey ? 2 : 0 };
this.computed.set(key, dispatch);
this.generated.add(helper);
lowerer.liftedFns.push(helper);
changed = true;
}
return { kind: "call", callee: dispatch.fn.name, args: computedRead ? [computedRead.value, computedRead.key] : computedWrite!.args, type: dispatch.fn.returnType, loc: expr.loc };
}
const read = expr.kind === "dynKeyGet" && expr.key.kind === "strLit" ? expr : null;
const write = expr.kind === "libCall" && expr.fn === "dyn.keySet" && expr.args[1]?.kind === "strLit" ? expr : null;
const name = read?.key.kind === "strLit" ? read.key.value : write?.args[1]?.kind === "strLit" ? write.args[1].value : null;
if (name !== null) {
const key = JSON.stringify([name, !!write, read?.optional ?? false]);
let dispatch = this.properties.get(key);
if (!dispatch) {
const loc = expr.loc;
const params = (write ? [0, 1] : [0]).map((i) => ({ localId: `p.${i}`, name: `p${i}`, type: DYN }));
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, args: [varRef("p.0", DYN, loc)], type: DYN, loc };
const fallback: IrExpr = read
? { ...read, value: bag }
: { ...write!, args: [bag, write!.args[1]!, varRef("p.1", DYN, loc)] };
const helper: IrFunction = {
name: `%dyn.class.property.${this.properties.size}`, params, returnType: write ? VOID : DYN,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
body: write
? [{ kind: "exprStmt", expr: fallback, loc }, { kind: "return", value: null, loc }]
: [{ kind: "return", value: fallback, loc }], loc,
};
dispatch = { name, write: !!write, fn: helper, classes: new Set() };
this.properties.set(key, dispatch);
this.generated.add(helper);
lowerer.liftedFns.push(helper);
changed = true;
}
return { kind: "call", callee: dispatch.fn.name, args: read ? [read.value] : [write!.args[0]!, write!.args[2]!], type: dispatch.fn.returnType, loc: expr.loc };
}
if (expr.kind !== "dynInvoke" || candidates(expr.method).length === 0) return expr;
const key = JSON.stringify([expr.method, expr.calleeName, expr.args.length]);
let dispatch = this.dispatches.get(key);
@@ -109,9 +222,139 @@ export class ClassDynamicDispatch {
changed = true;
}
}
for (const dispatch of this.properties.values()) {
for (const info of this.propertyCandidates(lowerer, dispatch.name)) {
if (dispatch.classes.has(info.def.name)) continue;
dispatch.classes.add(info.def.name);
const loc = dispatch.fn.loc;
const type: IrType = { kind: "object", className: info.def.name };
const receiver = varRef("p.0", DYN, loc);
const before = lowerer.diags.length;
let branch: IrStmt[];
try {
branch = this.propertyBody(lowerer, dispatch, info, { kind: "dynCheck", value: receiver, type, loc });
} catch (error) {
if (!(error instanceof PoisonError) || !info.decl) throw error;
const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
if (!fence) throw error;
branch = [fence];
}
dispatch.fn.body.unshift({
kind: "if", cond: {
kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
}, then: branch, else_: null, loc,
});
changed = true;
}
}
for (const dispatch of this.computed.values()) {
for (const className of this.boxed) {
if (dispatch.classes.has(className)) continue;
const info = lowerer.classes.get(className);
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
dispatch.classes.add(className);
const loc = dispatch.fn.loc;
const type: IrType = { kind: "object", className };
const value = varRef("p.0", DYN, loc);
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
const names = new Set([...info.fields.keys()].filter(isClassOwnEnumerableFieldName));
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
for (const method of owner.methods.keys()) {
if ((method.startsWith("get:") || method.startsWith("set:")) && isClassOwnEnumerableFieldName(method.slice(4))) names.add(method.slice(4));
}
}
const branch: IrStmt[] = [];
for (const name of names) {
const before = lowerer.diags.length;
let body: IrStmt[];
try {
body = this.propertyBody(lowerer, { ...dispatch, name }, info, receiver);
} catch (error) {
if (!(error instanceof PoisonError) || !info.decl) throw error;
const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
if (!fence) throw error;
body = [fence];
}
branch.push({ kind: "if", cond: {
kind: "strEq", left: varRef(dispatch.keyLocal, STRING, loc), right: { kind: "strLit", value: name, type: STRING, loc }, negated: false, type: BOOL, loc,
}, then: body, else_: null, loc });
}
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: {
kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
}, then: branch, else_: null, loc });
changed = true;
}
}
return changed;
}
/** The hidden bag is part of the native object layout, so every capsule
* shares it and normal class tracing/disposal owns its values. Insert it
* at the same prefix offset throughout a hierarchy, including classes
* collected before this untyped crossing was discovered. */
private ensurePropertyBag(info: ClassInfo): void {
if (info.fields.has(PROPERTY_BAG)) return;
let root = info;
while (root.base && !root.base.def.runtime) root = root.base;
const index = root.def.fields.length;
const add = (current: ClassInfo): void => {
if (!current.fields.has(PROPERTY_BAG)) {
current.fields.set(PROPERTY_BAG, DYN);
current.def.fields.splice(index, 0, { name: PROPERTY_BAG, type: DYN });
}
current.subclasses.forEach(add);
};
add(root);
}
private propertyCandidates(lowerer: Lowerer, name: string): ClassInfo[] {
if (!isClassOwnEnumerableFieldName(name)) return [];
return [...this.boxed].flatMap((className) => {
const info = lowerer.classes.get(className);
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) return [];
return info.fields.has(name) || findMethodOn(lowerer, info, `get:${name}`) || findMethodOn(lowerer, info, `set:${name}`) ? [info] : [];
});
}
private propertyBody(lowerer: Lowerer, dispatch: PropertyDispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] {
const { name, write } = dispatch;
const loc = dispatch.fn.loc;
const field = info.fields.get(name);
const member = `${write ? "set" : "get"}:${name}`;
const accessor = field ? null : findMethodOn(lowerer, info, member);
const fence = (): IrStmt[] => [{ kind: "runtimeFence", code: "SC2020", message: `${write ? "writing" : "reading"} '${name}' on this native class through an untyped value is not supported yet`, loc }];
const getRecord = (id: string) => lowerer.shapes.get(id);
const getUnion = (id: string) => lowerer.unions.get(id);
if (!write) {
if (!field && !accessor) return [{ kind: "return", value: dynUndefinedExpr(loc), loc }];
const type = field ?? accessor!.sig.ret;
if (!canConvertToDyn(type, getRecord, getUnion)) return fence();
const value: IrExpr = field
? { kind: "fieldGet", obj: receiver, className: info.def.name, field: name, type, loc }
: accessorCall(lowerer, info.def.name, member, receiver, [], type, loc);
const boxed = lowerer.coerceToExpected(value, DYN);
const mutable = (t: IrType): boolean => streamTypedRefEligible(t) ||
(t.kind === "union" && (getUnion(t.unionId)?.arms.some(mutable) ?? false));
if (boxed.kind === "dynFrom" && mutable(boxed.value.type)) boxed.liveRef = true;
return [{ kind: "return", value: boxed, loc }];
}
if (!field && !accessor) return [{ kind: "throw", value: {
kind: "libCall", fn: "error.new", args: [{
kind: "strLit", value: `Cannot set property ${name} of #<${info.def.jsName ?? info.def.name}> which has only a getter`, type: STRING, loc,
}], type: { kind: "object", className: "%TypeError" }, loc,
}, loc }];
const type = field ?? accessor!.sig.params[0]!.type;
const checkable = (t: IrType): boolean => isDynTypedRefType(t) || isUnitType(t) ||
(t.kind === "union" ? getUnion(t.unionId)?.arms.every(checkable) ?? false : canDynCheckTo(t, getRecord, getUnion));
if (!checkable(type)) return fence();
const value = lowerer.coerceToExpected(varRef("p.1", DYN, loc), type);
if (!typeEquals(value.type, type)) return fence();
const store: IrStmt = field
? { kind: "fieldSet", obj: receiver, className: info.def.name, field: name, value, loc }
: { kind: "exprStmt", expr: accessorCall(lowerer, info.def.name, member, receiver, [value], VOID, loc), loc };
return [store, { kind: "return", value: null, loc }];
}
private methodBody(lowerer: Lowerer, dispatch: Dispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] {
const { method, loc } = dispatch.source;
const fence = (): IrStmt[] => [{ kind: "runtimeFence", code: "SC2020", message: `calling '${method}' on this native class through an untyped value is not supported yet`, loc }];
@@ -8117,7 +8117,7 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
const member = lowerer.stdlibGlobalMember(access, "process");
if (member === null) return null;
const loc = locOf(call);
// process.on/once/off: the CLI event slice — the SIGINT/SIGTERM
// process.on/once/off: the CLI event slice — named OS
// signal handlers and the 'exit' hook. Signal listeners run as
// macrotasks at loop turns, replace the default disposition while
// registered (removing the last restores Ctrl-C death), and never
@@ -8152,12 +8152,17 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
loc,
};
}
// own(), not a bare index: the key is a USER-written event name,
// and `{ SIGINT: 2 }["__proto__"]` answers Object.prototype — an
// object flowed into a numLit and emitted itself into the C
// (test-event-emitter-special-event-names.js's process.on).
const SIGNALS: Record<string, number | undefined> = { SIGINT: 2, SIGTERM: 15 };
const signo = event !== null ? own(SIGNALS, event) : undefined;
if (event === "uncaughtException" || event === "uncaughtExceptionMonitor") {
if (!ts.isExpressionStatement(call.parent)) {
lowerer.unsupported("SC1090", call, "chaining process exception listener registration");
}
const cb = dcSubscriberArg(lowerer, call.arguments[1]!);
const monitor = boolLit(event === "uncaughtExceptionMonitor", loc);
return {
kind: "libCall", fn: isOff ? "process.offUncaughtException" : "process.onUncaughtException",
args: isOff ? [cb, monitor] : [cb, boolLit(member === "once", loc), monitor], type: VOID, loc,
};
}
// 'unhandledRejection': the listener crosses as a dyn function and
// the completed-checkpoint report dispatches it (reason, promise) per
// never-observed rejection instead of printing and exiting 1
@@ -8201,12 +8206,23 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
loc,
};
}
if (signo === undefined && event !== "exit") {
lowerer.noLowering(
`process.${member}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments[0]!,
'"message", "disconnect", "SIGINT", "SIGTERM", "exit", "warning", "unhandledRejection", and "rejectionHandled" are the supported process events (as literals)',
);
if (event === null || event.startsWith("SIG")) {
if (!ts.isExpressionStatement(call.parent)) {
lowerer.unsupported("SC1090", call, "chaining process signal listener registration");
}
const name = lowerer.coerceToExpected(lowerer.lowerExpr(call.arguments[0]!), STRING);
if (name.type.kind !== "string" && name.type.kind !== "dyn") {
lowerer.noLowering("process signal event name that is not a string", call.arguments[0]!);
}
const signal = name.type.kind === "string" ? name : { kind: "dynCheck" as const, value: name, type: STRING, loc };
const cb = dcSubscriberArg(lowerer, call.arguments[1]!);
return {
kind: "libCall", fn: isOff ? "process.offSignal" : "process.onSignal",
args: isOff ? [signal, cb] : [signal, cb, boolLit(member === "once", loc)], type: VOID, loc,
};
}
if (event !== "exit") {
lowerer.noLowering(`process.${member}("${event}", ...)`, call.arguments[0]!);
}
if (!ts.isExpressionStatement(call.parent)) {
lowerer.unsupported(
@@ -8225,7 +8241,7 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
// result; a non-function dyn value throws the catchable TypeError
// at REGISTRATION (Node's ERR_INVALID_ARG_TYPE moment).
{
const target = funcOf(signo !== undefined || event !== "exit" ? [] : [F64], VOID);
const target = funcOf([F64], VOID);
const exact =
cb.type.kind === "func" &&
cb.type.ret.kind === "void" &&
@@ -8256,20 +8272,6 @@ function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr
}
const param = cb.type.params[0];
const onceArg: IrExpr = { kind: "boolLit", value: member === "once", type: BOOL, loc };
if (signo !== undefined) {
if (param !== undefined) {
lowerer.unsupported(
"SC1090",
call.arguments[1]!,
"signal listeners with parameters (the signal name argument has no lowering — use ())",
);
}
const sig: IrExpr = { kind: "numLit", value: signo, type: F64, loc };
if (isOff) {
return { kind: "libCall", fn: "process.offSignal", args: [sig, cb], type: VOID, loc };
}
return { kind: "libCall", fn: "process.onSignal", args: [sig, cb, onceArg], type: VOID, loc };
}
if (param !== undefined && param.kind !== "f64") {
lowerer.unsupported(
"SC1090",
+15 -8
View File
@@ -3811,14 +3811,10 @@ export type IrLibFn =
| "process.cpuPrevValidate"
| "process.rusage"
| "process.activeResources"
/** Process signal events — process.on/once/off("SIGINT" | "SIGTERM").
* args: [signo f64 (the frontend bakes the POSIX number), cb, once
* bool] for on; [signo, cb] for off (cb borrowed, removed by pointer
* identity — Node's removeListener contract). Handlers run as
* macrotasks at loop turns; watching replaces the default disposition
* and removing the last listener restores it; signal listeners never
* keep the loop alive (Node). Zero-param callbacks only (the ambient
* shape). Never throw. */
/** Named process signal events: [name string, callback dyn, once bool]
* or [name, callback] for removal. The runtime resolves platform signal
* numbers and delivers (name, number). Listeners do not keep the loop
* alive. Invalid callbacks and uncatchable signals throw. */
| "process.onSignal"
| "process.offSignal"
/** The process 'exit' event — process.on/once/off("exit"). args: [cb,
@@ -4502,6 +4498,9 @@ export type IrLibFn =
* Node's once); off/removeListener remove by closure identity, the
* offWarning stance. Throws Node's ERR_INVALID_ARG_TYPE on a
* non-function. */
/** Uncaught exception handlers and monitors: [callback dyn, once, monitor]. */
| "process.onUncaughtException"
| "process.offUncaughtException"
| "process.onUnhandledRejection"
| "process.offUnhandledRejection"
/** process.on/once/off('rejectionHandled', fn): the sibling registry.
@@ -6230,6 +6229,9 @@ export function isClassOwnEnumerableFieldName(name: string): boolean {
return !name.startsWith("#") && !name.startsWith("%");
}
/** Compiler-owned storage for properties added through untyped references. */
export const DYN_CLASS_PROPERTIES = "%dynProperties";
/** A class capsule can always preserve its exact native identity. Its
* optional property view additionally needs converters in both directions;
* fields such as Maps may remain opaque without preventing the round trip. */
@@ -6907,6 +6909,7 @@ export function moduleUsesDynInvoke(mod: IrModule): boolean {
export function moduleUsesDynAsync(mod: IrModule): boolean {
const fns = new Set([
"async.awaitDyn", "timers.immediatePromise",
"process.onUncaughtException", "process.offUncaughtException",
"process.onUnhandledRejection", "process.offUnhandledRejection",
"process.onRejectionHandled", "process.offRejectionHandled",
"process.onWarning", "process.offWarning", "process.emitWarning",
@@ -7697,6 +7700,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dc.tcTraceSync",
"dc.tcTraceCallback",
"dc.tcTracePromise",
"process.onUncaughtException",
"process.offUncaughtException",
"process.onSignal",
"process.offSignal",
"process.onUnhandledRejection",
"process.onRejectionHandled",
"async.awaitDyn",
+4 -2
View File
@@ -192,8 +192,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// Signal listeners are zero-param (the ambient shape); exit/stdin
// callbacks carry program-dependent one-param shapes — null slots, the
// libCall case checks them (child.onExit precedent).
"process.onSignal": { argTypes: [F64, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"process.offSignal": { argTypes: [F64, { kind: "func", params: [], ret: VOID }], result: VOID },
"process.onSignal": { argTypes: [STRING, DYN, BOOL], result: VOID },
"process.offSignal": { argTypes: [STRING, DYN], result: VOID },
"process.onExit": { argTypes: [null, BOOL], result: VOID },
"process.offExit": { argTypes: [null], result: VOID },
"stdin.onData": { argTypes: [null, BOOL], result: VOID },
@@ -1383,6 +1383,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dc.tcTraceSync": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN },
"dc.tcTraceCallback": { argTypes: [F64, DYN, F64, DYN, DYN, DYN], result: DYN },
"dc.tcTracePromise": { argTypes: [F64, DYN, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } },
"process.onUncaughtException": { argTypes: [DYN, BOOL, BOOL], result: VOID },
"process.offUncaughtException": { argTypes: [DYN, BOOL], result: VOID },
"process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID },
"process.offUnhandledRejection": { argTypes: [DYN], result: VOID },
"process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID },
@@ -11577,6 +11577,102 @@
"<repo>/tests/corpus/primitive-and-function-descriptors.js"
],
"diags": []
},
"<repo>/tests/corpus/3068-url-checked-identity.ts": {
"order": [
"<repo>/tests/corpus/3068-url-checked-identity.ts"
],
"diags": []
},
"<repo>/tests/corpus/3069-set-checked-seed.js": {
"order": [
"<repo>/tests/corpus/3069-set-checked-seed.js"
],
"diags": []
},
"<repo>/tests/corpus/3070-checked-object-accessors.js": {
"order": [
"<repo>/tests/corpus/3070-checked-object-accessors.js"
],
"diags": []
},
"<repo>/tests/corpus/3071-checked-specialization-narrowing.js": {
"order": [
"<repo>/tests/corpus/3071-checked-specialization-narrowing.js"
],
"diags": []
},
"<repo>/tests/corpus/3072-weak-process-streams.js": {
"order": [
"<repo>/tests/corpus/3072-weak-process-streams.js"
],
"diags": []
},
"<repo>/tests/corpus/3073-optional-callable-module.js": {
"order": [
"<repo>/tests/corpus/3073-optional-callable-module.js"
],
"diags": []
},
"<repo>/tests/corpus/native-class-properties.cjs": {
"order": [
"<repo>/tests/corpus/native-class-properties.cjs"
],
"diags": []
},
"<repo>/tests/corpus/native-class-property-cycles.cjs": {
"order": [
"<repo>/tests/corpus/native-class-property-cycles.cjs"
],
"diags": []
},
"<repo>/tests/corpus/native-class-property-order.cjs": {
"order": [
"<repo>/tests/corpus/native-class-property-order.cjs"
],
"diags": []
},
"<repo>/tests/corpus/process-named-signals.cjs": {
"order": [
"<repo>/tests/corpus/process-named-signals.cjs"
],
"diags": []
},
"<repo>/tests/corpus/process-uncaught-handler-throws.cjs": {
"order": [
"<repo>/tests/corpus/process-uncaught-handler-throws.cjs"
],
"diags": []
},
"<repo>/tests/corpus/process-uncaught-loop.cjs": {
"order": [
"<repo>/tests/corpus/process-uncaught-loop.cjs"
],
"diags": []
},
"<repo>/tests/corpus/process-uncaught-module.mjs": {
"order": [
"<repo>/tests/corpus/process-uncaught-module.mjs"
],
"diags": []
},
"<repo>/tests/corpus/process-uncaught-monitor.cjs": {
"order": [
"<repo>/tests/corpus/process-uncaught-monitor.cjs"
],
"diags": []
},
"<repo>/tests/corpus/process-uncaught-rejection.cjs": {
"order": [
"<repo>/tests/corpus/process-uncaught-rejection.cjs"
],
"diags": []
},
"<repo>/tests/corpus/process-uncaught-sync.cjs": {
"order": [
"<repo>/tests/corpus/process-uncaught-sync.cjs"
],
"diags": []
}
}
}
+40 -7
View File
@@ -156,6 +156,7 @@ typedef struct ScrFiber ScrFiber;
struct ScrPromise {
size_t rc;
int state;
bool top_level_handled; /* uncaughtException handled entry evaluation */
/* Payload (fulfillment value or rejection reason), ScrExcCell-style.
* trace_fn is non-NULL iff the REF payload type carries a cycle header
* (a promise settled with a cycle-capable value can be a cycle member —
@@ -1553,7 +1554,7 @@ int scr_promise_finish_top_level(ScrPromise *p) {
return 13;
}
scr_prom_observe(p);
return p->state == SCR_PROM_REJECTED ? 1 : 0;
return p->state == SCR_PROM_REJECTED && !p->top_level_handled ? 1 : 0;
}
void scr_promise_rethrow_top_level(ScrPromise *p) {
@@ -2487,14 +2488,13 @@ void scr_loop_set_stream(bool (*pending)(void), void (*dispatch)(void)) {
* microtask checkpoints between macrotasks). */
bool scr_loop_has_ready(void) { return scr_ready_len > 0; }
bool scr_loop_run(ScrPromise *top_level) {
static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
/* The FIRST checkpoint after the synchronous main body runs promise
* jobs BEFORE the first tick drain: Node's main-module evaluation is
* itself awaited (the runMain continuation is a microtask queued after
* the body's own), so microtasks scheduled during the body beat ticks
* scheduled during the body exactly once, at startup — differentially
* pinned. Every later checkpoint drains ticks first. */
bool first_checkpoint = true;
bool rejection_failed = false;
for (;;) {
if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
@@ -2860,14 +2860,14 @@ bool scr_loop_run(ScrPromise *top_level) {
scr_firing_refresh = false;
}
((void (*)(ScrClosure *))t.cb->fn)(t.cb);
if (t.id != 0 && t.repeat_ms > 0 && !scr_firing_cleared && !scr_exc_pending()) {
if (t.id != 0 && t.repeat_ms > 0 && !scr_firing_cleared) {
/* Re-arm relative to the post-callback clock (libuv's uv_timer
* repeat behavior: no catch-up bursts after a slow callback). The
* ref state carries across ticks (a self-unref'd interval stays
* unref'd). */
ScrTimer again = {scr_now_ms() + t.repeat_ms, scr_timer_seq++, t.cb, t.repeat_ms, t.id, scr_firing_reffed, t.delay_ms};
scr_timer_push(again);
} else if (t.id != 0 && scr_firing_refresh && !scr_firing_cleared && !scr_exc_pending()) {
} else if (t.id != 0 && scr_firing_refresh && !scr_firing_cleared) {
/* refresh() from inside the one-shot's own callback: re-arm to
* now + the original delay, ref state carried (Node's
* Timeout.refresh — the timer fires again). */
@@ -2916,6 +2916,32 @@ bool scr_loop_run(ScrPromise *top_level) {
}
}
}
return rejection_failed;
}
bool scr_loop_run(ScrPromise *top_level) {
bool first_checkpoint = true;
bool rejection_failed;
for (;;) {
rejection_failed = scr_loop_run_pass(top_level && !top_level->top_level_handled ? top_level : NULL, first_checkpoint);
first_checkpoint = false;
if (scr_exc_pending()) {
if (scr_exc_handle_uncaught(false)) continue;
break; /* main reports the original or handler-thrown exception */
}
if (rejection_failed) break;
if (top_level && top_level->state == SCR_PROM_REJECTED && !top_level->top_level_handled) {
scr_promise_rethrow(top_level);
if (scr_exc_handle_uncaught(true)) {
top_level->top_level_handled = true;
scr_prom_observe(top_level);
continue;
}
scr_discard_unhandled_rejections();
break;
}
break;
}
/* Exit can now leave UNREF'd timers armed in the heap (ordinary
* exhaustion, a fatal module root, or an unhandled rejection). They
* never fire, so release their closures here or the RC audit counts
@@ -3000,7 +3026,15 @@ bool scr_report_unhandled_rejections(void) {
* (scr_prom_observe). */
p->rejection_observed = true;
p->reported_unhandled = true;
if (!scr_urj_deliver_fn(p)) crashed = true;
if (!scr_urj_deliver_fn(p) && !scr_exc_handle_uncaught(false)) crashed = true;
} else if (scr_uncaught_exception_hook != NULL) {
scr_promise_rethrow(p);
if (scr_exc_handle_uncaught(true)) {
p->rejection_observed = true;
p->reported_unhandled = true;
} else {
crashed = true;
}
} else if (!any) {
any = true;
fflush(stdout);
@@ -3038,7 +3072,6 @@ bool scr_report_unhandled_rejections(void) {
scr_nunhandled = remaining;
if (crashed) {
scr_exc_print_uncaught();
scr_exit_code_note(1);
return true;
}
if (scr_island_rejections_fn != NULL) {
+108 -29
View File
@@ -149,25 +149,39 @@ ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args) {
/* The two rejection-event registries share one shape: listeners with a
* `once` flag (auto-removed after one delivery, Node's once) and
* identity-based removal (the offWarning stance). */
typedef struct {
size_t refs;
bool fired;
} ScrRejState;
typedef struct {
ScrDyn *fn;
bool once;
uint64_t id;
ScrRejState *state; /* snapshots share the once-fired state */
} ScrRejListener;
static void scr_rej_release(ScrRejListener *l) {
scr_dyn_release(l->fn);
if (--l->state->refs == 0) free(l->state);
}
static uint64_t scr_rej_id = 0;
static ScrRejListener *scr_urj_listeners = NULL;
static size_t scr_nurj = 0, scr_urj_cap = 0;
static ScrRejListener *scr_rjh_listeners = NULL;
static size_t scr_nrjh = 0, scr_rjh_cap = 0;
static void scr_urj_teardown(void) {
for (size_t i = 0; i < scr_nurj; i++) scr_dyn_release(scr_urj_listeners[i].fn);
for (size_t i = 0; i < scr_nurj; i++) scr_rej_release(&scr_urj_listeners[i]);
free(scr_urj_listeners);
scr_urj_listeners = NULL;
scr_nurj = scr_urj_cap = 0;
}
static void scr_rjh_teardown(void) {
for (size_t i = 0; i < scr_nrjh; i++) scr_dyn_release(scr_rjh_listeners[i].fn);
for (size_t i = 0; i < scr_nrjh; i++) scr_rej_release(&scr_rjh_listeners[i]);
free(scr_rjh_listeners);
scr_rjh_listeners = NULL;
scr_nrjh = scr_rjh_cap = 0;
@@ -191,16 +205,20 @@ static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *
*list = realloc(*list, *cap * sizeof **list);
if (!*list) scr_ad_oom();
}
(*list)[(*n)++] = (ScrRejListener){scr_dyn_retain(fn), once};
ScrRejState *state = calloc(1, sizeof *state);
if (!state) scr_ad_oom();
state->refs = 1;
(*list)[(*n)++] = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state};
}
static void scr_rej_remove(ScrRejListener *list, size_t *n, ScrDyn *fn) {
for (size_t i = 0; i < *n; i++) {
for (size_t left = *n; left > 0; left--) {
size_t i = left - 1;
ScrDyn *l = list[i].fn;
bool same = l == fn || (l->kind == SCR_DYN_FUNC && fn->kind == SCR_DYN_FUNC &&
scr_dyn_strict_eq(l, fn));
if (same) {
scr_dyn_release(l);
scr_rej_release(&list[i]);
memmove(list + i, list + i + 1, (*n - i - 1) * sizeof *list);
(*n)--;
return;
@@ -251,35 +269,96 @@ void scr_process_off_rejection_handled(ScrDyn *fn) {
scr_rjh_sync_hook();
}
/* One registry pass: call every listener with `args`, removing once-
* listeners BEFORE their call (Node's once removes at dispatch, so a
* re-registration inside the listener sticks). The registry is accessed
* THROUGH its pointers per step — a listener that registers can realloc
* the array mid-pass. Returns false when a listener threw (the caller's
* crash path). */
/* Snapshot dispatch preserves EventEmitter mutation rules: removals do
* not skip a pending listener, additions wait for the next delivery, and
* once entries leave the live list before invoking user code. */
static bool scr_rej_fire(ScrRejListener **list, size_t *n, ScrDyn **args, size_t argc) {
size_t i = 0;
bool ok = true;
while (i < *n && ok) {
/* Own +1 across the call: a once-removal (here) or the listener
* removing itself (off inside the body) must not free a running
* function. */
ScrDyn *fn = scr_dyn_retain((*list)[i].fn);
if ((*list)[i].once) {
scr_dyn_release((*list)[i].fn);
memmove(*list + i, *list + i + 1, (*n - i - 1) * sizeof **list);
(*n)--;
} else {
i++;
}
ScrDyn *r = scr_dyn_call(fn, args, argc, "listener");
if (r == NULL) ok = false;
else scr_dyn_release(r);
scr_dyn_release(fn);
size_t count = *n;
ScrRejListener *snap = count ? malloc(count * sizeof *snap) : NULL;
if (count && !snap) scr_ad_oom();
for (size_t i = 0; i < count; i++) {
snap[i] = (*list)[i];
scr_dyn_retain(snap[i].fn);
snap[i].state->refs++;
}
bool ok = true;
for (size_t i = 0; i < count; i++) {
if (ok && !(snap[i].once && snap[i].state->fired)) {
if (snap[i].once) {
snap[i].state->fired = true;
for (size_t j = 0; j < *n; j++) {
if ((*list)[j].id == snap[i].id) {
scr_rej_release(&(*list)[j]);
memmove(*list + j, *list + j + 1, (*n - j - 1) * sizeof **list);
(*n)--;
break;
}
}
}
ScrDyn *r = scr_dyn_call(snap[i].fn, args, argc, "listener");
ok = !scr_exc_pending();
scr_dyn_release(r);
}
scr_rej_release(&snap[i]);
}
free(snap);
return ok;
}
/* Uncaught exception listeners share the checked-dynamic callback ABI.
* Monitors run first but do not handle the exception by themselves. */
static ScrRejListener *scr_uncaught_ls[2];
static size_t scr_uncaught_n[2], scr_uncaught_cap[2];
static int scr_uncaught_dispatch(bool from_promise);
static void scr_uncaught_sync_hook(void) {
scr_uncaught_exception_hook = (scr_uncaught_n[0] || scr_uncaught_n[1]) ? scr_uncaught_dispatch : NULL;
}
static void scr_uncaught_teardown(void) {
for (size_t k = 0; k < 2; k++) {
for (size_t i = 0; i < scr_uncaught_n[k]; i++) scr_rej_release(&scr_uncaught_ls[k][i]);
free(scr_uncaught_ls[k]);
scr_uncaught_ls[k] = NULL;
scr_uncaught_n[k] = scr_uncaught_cap[k] = 0;
}
scr_uncaught_sync_hook();
}
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
static bool armed = false;
if (!armed) { armed = true; atexit(scr_uncaught_teardown); }
size_t k = monitor ? 1 : 0;
scr_rej_push(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, once);
scr_uncaught_sync_hook();
}
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
size_t k = monitor ? 1 : 0;
scr_rej_remove(scr_uncaught_ls[k], &scr_uncaught_n[k], fn);
scr_uncaught_sync_hook();
}
static int scr_uncaught_dispatch(bool from_promise) {
ScrCaught *caught = scr_exc_take();
ScrDyn *error = scr_caught_to_dyn(caught);
const char *origin = from_promise ? "unhandledRejection" : "uncaughtException";
ScrStr *text = scr_str_new(origin, strlen(origin));
ScrDyn *args[2] = {error, scr_dyn_new_str(text)};
scr_str_release(text);
bool ok = scr_rej_fire(&scr_uncaught_ls[1], &scr_uncaught_n[1], args, 2);
bool handled = ok && scr_uncaught_n[0] > 0;
if (handled) ok = scr_rej_fire(&scr_uncaught_ls[0], &scr_uncaught_n[0], args, 2);
scr_dyn_release(args[0]);
scr_dyn_release(args[1]);
scr_uncaught_sync_hook();
if (ok && !handled) scr_rethrow(caught);
scr_caught_release(caught);
return !ok ? -1 : handled ? 1 : 0;
}
/* Dispatch one unhandled rejection to the registered listeners —
* (reason, promise), Node's signature. A listener throw is an uncaught
* exception (Node crashes there too): the caller prints it and exits 1.
+123 -97
View File
@@ -1,5 +1,5 @@
/* Process events and stdin streaming — the OPTIONAL half of the event
* loop: signal handlers (process.on/once/off of SIGINT/SIGTERM), the
* loop: signal handlers (named process.on/once/off signal handlers), the
* process 'exit' event, and the piped-stdin surface ('data'/'end'/'error'
* listeners plus the for-await chunk source). This translation unit links
* ONLY into binaries whose IR uses those surfaces (moduleUsesProcessEvents
@@ -59,7 +59,7 @@ void scr_child_err_thunk_error(ScrClosure *cb, ScrStr *msg) {
}
#endif
/* ── process signal events (process.on("SIGINT" | "SIGTERM")) ─────────
/* ── process signal events (process.on(name, listener)) ─────────
* Classic self-pipe integration: a watched signal's sigaction handler
* (installed WITHOUT SA_RESTART) sets a per-signal flag and writes one
* byte into the wake pipe. The loop drains flags at every turn and the
@@ -75,8 +75,10 @@ void scr_child_err_thunk_error(ScrClosure *cb, ScrStr *msg) {
* kernel coalesces pending non-RT signals the same way). */
typedef struct {
ScrClosure *cb; /* owned */
ScrDyn *cb; /* owned; checked callable */
ScrStr *name; /* aliases of one signum remain distinct event names */
bool once;
uint64_t id;
} ScrSigListener;
typedef struct ScrSigReg {
@@ -91,18 +93,20 @@ typedef struct ScrSigReg {
struct ScrSigReg *next;
} ScrSigReg;
/* Classic signal numbers only (SIGINT 2 / SIGTERM 15 today; every POSIX
* classic fits) — NSIG is not visible under strict _XOPEN_SOURCE. */
/* Every named classic POSIX signal fits; NSIG is hidden by strict
* _XOPEN_SOURCE. Unknown SIG-prefixed names use the unwatched slot zero. */
#define SCR_SIG_MAX 32
static ScrSigReg *scr_sig_regs = NULL;
static uint64_t scr_sig_id = 0;
static size_t scr_sig_watched = 0; /* watched signal COUNT (regs) */
static volatile sig_atomic_t scr_sig_flag[SCR_SIG_MAX];
static volatile sig_atomic_t scr_sig_any = 0;
static int scr_wake_pipe[2] = {-1, -1};
static void scr_sig_handler(int sig) {
if (sig < 0 || sig >= SCR_SIG_MAX) return;
if (sig <= 0 || sig >= SCR_SIG_MAX) return;
int saved_errno = errno;
#ifdef _WIN32
/* msvcrt reset the disposition to SIG_DFL before this call (SysV
* semantics); re-arm so the next delivery reaches us too. Only ever
@@ -116,6 +120,7 @@ static void scr_sig_handler(int sig) {
ssize_t ignored = write(scr_wake_pipe[1], "s", 1);
(void)ignored; /* a full pipe still wakes the poller */
}
errno = saved_errno;
}
static void scr_sig_reg_drop(ScrSigReg *reg);
@@ -128,7 +133,10 @@ static void scr_sig_reg_drop(ScrSigReg *reg);
static void scr_sig_cleanup(void) {
while (scr_sig_regs) {
ScrSigReg *reg = scr_sig_regs;
for (size_t i = 0; i < reg->n; i++) scr_closure_release(reg->ls[i].cb);
for (size_t i = 0; i < reg->n; i++) {
scr_dyn_release(reg->ls[i].cb);
scr_str_release(reg->ls[i].name);
}
reg->n = 0;
scr_sig_reg_drop(reg);
}
@@ -159,143 +167,161 @@ static void scr_wake_pipe_drain(void) {
while (read(scr_wake_pipe[0], buf, sizeof buf) > 0) {}
}
void scr_signal_on(double signum, ScrClosure *cb /*moves*/, bool once) {
#ifdef __wasi__
(void)signum;
(void)once;
scr_closure_release(cb);
/* The compiler rejects this capability before linking. This guard keeps
* the optional events unit honest if hand-authored IR reaches it. */
scr_trap("scriptc: internal error: OS signal surface reached on WASI\n");
return;
#else
int sig = (int)signum;
if (sig <= 0 || sig >= SCR_SIG_MAX) {
scr_closure_release(cb); /* frontend only emits classic signals */
return;
/* A computed process-event name is accepted here only for the signal
* family. Other process-event families retain their explicit lowerings. */
static bool scr_signal_check(ScrStr *name, ScrDyn *cb) {
if (cb->kind != SCR_DYN_FUNC) {
const char *msg = "The \"listener\" argument must be of type function";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, strlen(msg), "ERR_INVALID_ARG_TYPE");
return false;
}
if (name->len < 3 || memcmp(name->data, "SIG", 3) != 0) {
const char *msg = "scriptc: computed process event names currently support the SIG signal family";
scr_throw_error_msg(SCR_ERR_ERROR, msg, strlen(msg));
return false;
}
return true;
}
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
if (!scr_signal_check(name, cb)) return;
#ifdef __wasi__
(void)once;
scr_trap("scriptc: internal error: OS signal surface reached on WASI\n");
#else
int sig = scr_signal_from_name(name);
if (sig < 0) sig = 0; /* e.g. SIGBREAK on POSIX: ordinary inert event */
ScrSigReg *reg = scr_sig_regs;
while (reg && reg->sig != sig) reg = reg->next;
if (!reg) {
reg = calloc(1, sizeof *reg);
if (!reg) scr_events_oom();
reg->sig = sig;
if (sig > 0) {
bool failed = sig >= SCR_SIG_MAX;
#ifdef _WIN32
/* The Windows CRT rejects non-CRT signal numbers via its fatal
* invalid-parameter handler. Keep these forms explicitly fenced. */
if (sig != SIGINT && sig != SIGILL && sig != SIGABRT && sig != SIGFPE &&
sig != SIGSEGV && sig != SIGTERM && sig != SIGBREAK) failed = true;
if (!failed) {
reg->prev = signal(sig, scr_sig_handler);
failed = reg->prev == SIG_ERR;
}
#else
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = scr_sig_handler;
sigemptyset(&sa.sa_mask);
if (!failed) failed = sigaction(sig, &sa, &reg->prev) != 0;
#endif
if (failed) {
free(reg);
const char *msg = "uv_signal_start EINVAL";
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, strlen(msg), "EINVAL");
return;
}
scr_sig_watched++;
scr_wake_pipe_init();
}
reg->next = scr_sig_regs;
scr_sig_regs = reg;
scr_sig_watched++;
scr_wake_pipe_init();
#ifdef _WIN32
reg->prev = signal(sig, scr_sig_handler);
if (reg->prev == SIG_ERR) reg->prev = SIG_DFL;
#else
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = scr_sig_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* NO SA_RESTART: idle sleeps must EINTR */
sigaction(sig, &sa, &reg->prev);
#endif
}
if (reg->n == reg->cap) {
reg->cap = reg->cap ? reg->cap * 2 : 2;
reg->ls = realloc(reg->ls, reg->cap * sizeof *reg->ls);
if (!reg->ls) scr_events_oom();
}
reg->ls[reg->n].cb = cb;
reg->ls[reg->n].once = once;
reg->n++;
reg->ls[reg->n++] = (ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id};
#endif
}
static void scr_sig_reg_drop(ScrSigReg *reg) {
if (reg->sig > 0) {
#ifdef _WIN32
signal(reg->sig, reg->prev); /* default disposition back */
#elif defined(__wasi__)
/* No signal disposition exists in WASI Preview 1. The compiler refuses
* signal registrations, so this arm is only the cleanup safety net. */
#else
sigaction(reg->sig, &reg->prev, NULL); /* default disposition back */
signal(reg->sig, reg->prev);
#elif !defined(__wasi__)
sigaction(reg->sig, &reg->prev, NULL);
#endif
scr_sig_watched--;
scr_sig_flag[reg->sig] = 0;
}
ScrSigReg **link = &scr_sig_regs;
while (*link != reg) link = &(*link)->next;
*link = reg->next;
scr_sig_watched--;
free(reg->ls);
free(reg);
}
void scr_signal_off(double signum, ScrClosure *cb /*borrowed*/) {
#ifdef __wasi__
(void)signum;
(void)cb;
scr_trap("scriptc: internal error: OS signal surface reached on WASI\n");
return;
#else
int sig = (int)signum;
static void scr_sig_remove(ScrSigReg *reg, size_t i) {
scr_dyn_release(reg->ls[i].cb);
scr_str_release(reg->ls[i].name);
memmove(reg->ls + i, reg->ls + i + 1, (reg->n - i - 1) * sizeof *reg->ls);
if (--reg->n == 0) scr_sig_reg_drop(reg);
}
void scr_signal_off(ScrStr *name, ScrDyn *cb) {
if (!scr_signal_check(name, cb)) return;
int sig = scr_signal_from_name(name);
if (sig < 0) sig = 0;
ScrSigReg *reg = scr_sig_regs;
while (reg && reg->sig != sig) reg = reg->next;
if (!reg) return;
for (size_t i = 0; i < reg->n; i++) {
if (reg->ls[i].cb == cb) { /* first match, like Node's removeListener */
scr_closure_release(reg->ls[i].cb);
memmove(reg->ls + i, reg->ls + i + 1, (reg->n - i - 1) * sizeof *reg->ls);
reg->n--;
if (reg->n == 0) scr_sig_reg_drop(reg);
/* EventEmitter removes the most recently registered matching listener. */
for (size_t i = reg->n; i > 0; i--) {
ScrSigListener *l = &reg->ls[i - 1];
if (scr_str_eq(l->name, name) && scr_dyn_strict_eq(l->cb, cb)) {
scr_sig_remove(reg, i - 1);
return;
}
}
#endif
}
/* One dispatch pass: every flagged signal fires its listener SNAPSHOT
* (Node emits over a copy — a listener removed mid-emit still runs for
* this delivery; one added mid-emit waits for the next). `once` entries
* leave the live list BEFORE their callback runs; an emptied list
* restores the default disposition, so the next delivery of that signal
* kills the process — Node-exact. A throw stops the pass and leaves the
* exception for the loop. */
/* Snapshot all pending deliveries before invoking user code. No live
* registry pointer survives a callback: off() can remove this signal or
* another pending signal and free either registry. */
static void scr_signals_drain(void) {
if (!scr_sig_any) return;
scr_sig_any = 0;
ScrSigReg *reg = scr_sig_regs;
while (reg) {
ScrSigReg *next = reg->next; /* reg may drop below */
if (scr_sig_flag[reg->sig]) {
scr_sig_flag[reg->sig] = 0;
size_t n = reg->n;
ScrSigListener *snap = malloc(n * sizeof *snap);
if (!snap) scr_events_oom();
for (size_t i = 0; i < n; i++) {
snap[i] = reg->ls[i];
scr_closure_retain(snap[i].cb);
}
bool dropped = false;
for (size_t i = 0; i < n; i++) {
if (snap[i].once && !dropped) {
/* remove from the live list before invoking */
ScrSigListener *snap = NULL;
size_t n = 0;
for (ScrSigReg *reg = scr_sig_regs; reg; reg = reg->next) {
if (reg->sig <= 0 || !scr_sig_flag[reg->sig]) continue;
scr_sig_flag[reg->sig] = 0;
snap = realloc(snap, (n + reg->n) * sizeof *snap);
if (!snap) scr_events_oom();
for (size_t i = 0; i < reg->n; i++) {
snap[n] = reg->ls[i];
scr_dyn_retain(snap[n].cb);
scr_str_retain(snap[n].name);
n++;
}
}
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) {
if (snap[i].once) {
for (ScrSigReg *reg = scr_sig_regs; reg; reg = reg->next) {
bool found = false;
for (size_t j = 0; j < reg->n; j++) {
if (reg->ls[j].cb == snap[i].cb) {
scr_closure_release(reg->ls[j].cb);
memmove(reg->ls + j, reg->ls + j + 1, (reg->n - j - 1) * sizeof *reg->ls);
reg->n--;
if (reg->ls[j].id == snap[i].id) {
scr_sig_remove(reg, j);
found = true;
break;
}
}
if (reg->n == 0) {
scr_sig_reg_drop(reg);
dropped = true;
}
if (found) break;
}
if (!scr_exc_pending()) {
((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb);
}
scr_closure_release(snap[i].cb);
}
free(snap);
if (scr_exc_pending()) return;
ScrDyn *args[2] = {scr_dyn_new_str(snap[i].name), scr_dyn_new_num(scr_signal_from_name(snap[i].name))};
ScrDyn *result = scr_dyn_call(snap[i].cb, args, 2, "listener");
scr_dyn_release(result);
scr_dyn_release(args[0]);
scr_dyn_release(args[1]);
}
reg = next;
scr_dyn_release(snap[i].cb);
scr_str_release(snap[i].name);
}
free(snap);
}
/* ── process.stdin events and async iteration ─────────────────────────
+12 -1
View File
@@ -362,8 +362,19 @@ static SCR_TL int scr_exit_code_hint = 0;
void scr_exit_code_note(int code) { scr_exit_code_hint = code; }
int scr_exit_code_hint_get(void) { return scr_exit_code_hint; }
int (*scr_uncaught_exception_hook)(bool from_promise) = NULL;
static bool scr_uncaught_handler_failed = false;
bool scr_exc_handle_uncaught(bool from_promise) {
if (!scr_exc_pending()) return true;
if (!scr_uncaught_exception_hook || scr_uncaught_handler_failed) return false;
int result = scr_uncaught_exception_hook(from_promise);
if (result < 0) scr_uncaught_handler_failed = true;
return result > 0;
}
void scr_exc_print_uncaught(void) {
scr_exit_code_note(1);
scr_exit_code_note(scr_uncaught_handler_failed ? 7 : 1);
/* Settle any runtime-internal stdout fragment before the stderr line when
* both share an fd. JavaScript-visible writes already flush themselves. */
fflush(stdout);
+24 -3
View File
@@ -353,7 +353,10 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) {
visit(d->v.proxy.target, ctx);
visit(d->v.proxy.handler, ctx);
break;
case SCR_DYN_TYPED_REF: visit(d->v.typed_ref.materialized, ctx); break;
case SCR_DYN_TYPED_REF:
visit(d->v.typed_ref.materialized, ctx);
if (d->v.typed_ref.traced) visit(d->v.typed_ref.ptr, ctx);
break;
case SCR_DYN_HANDLE:
if (d->v.handle.traced) visit(d->v.handle.ptr, ctx);
break;
@@ -469,7 +472,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
cast->release(cast->ptr);
free(cast);
}
d->v.typed_ref.release(d->v.typed_ref.ptr);
if (!collected || !d->v.typed_ref.traced) d->v.typed_ref.release(d->v.typed_ref.ptr);
break;
case SCR_DYN_PROXY:
if (!collected) {
@@ -1119,6 +1122,12 @@ static bool scr_dyn_canonical_own_index(const ScrStr *key, size_t length);
ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) {
if (scr_dyn_class_reflection_fence(value)) return NULL;
if (value->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(value);
ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_own_descriptor(view, key);
scr_dyn_release(view);
return result;
}
if (value->kind == SCR_DYN_FUNC) {
if (value->v.fn.clo->props) {
ScrDyn *table = (ScrDyn *)scr_box_get_ref(value->v.fn.clo->props);
@@ -1354,9 +1363,21 @@ ScrDyn *scr_dyn_new_typed_ref(
d->v.typed_ref.commit = commit;
d->v.typed_ref.materialized = NULL;
d->v.typed_ref.casts = NULL;
d->v.typed_ref.traced = false;
return d;
}
ScrDyn *scr_dyn_new_typed_ref_traced(
void *ptr, void *(*retain)(void *), void (*release)(void *),
const char *type_key, size_t type_key_len,
ScrDyn *(*materialize)(void *),
void (*commit)(void *, const ScrDyn *)) {
ScrDyn *value = scr_dyn_new_typed_ref(ptr, retain, release, type_key,
type_key_len, materialize, commit);
value->v.typed_ref.traced = true;
return value;
}
bool scr_dyn_typed_ref_is(
const ScrDyn *d, const char *type_key, size_t type_key_len) {
return d && d->kind == SCR_DYN_TYPED_REF &&
@@ -2352,7 +2373,7 @@ ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key) {
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM) {
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM && target->kind != SCR_DYN_TYPED_REF) {
static const char msg[] = "Object.getOwnPropertyDescriptor on this value is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
+1 -1
View File
@@ -850,7 +850,7 @@ static int scr_signal_by_name(const char *name) {
/* The table above for other units (scr_child.c's child.kill shares Node's
* one signal-name story): the resolved number, or -1 for unknown names. */
int scr_signal_from_name(const ScrStr *signal) {
return scr_signal_by_name(signal->data);
return strlen(signal->data) == signal->len ? scr_signal_by_name(signal->data) : -1;
}
/* The reverse walk, for spawnSync's result.signal: the FIRST name with
+14 -2
View File
@@ -2231,6 +2231,12 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c);
* "Uncaught <value>" to stderr (strings raw, numbers JS-exact, booleans
* true/false, ref payloads as "[object]"), and releases the payload. */
void scr_exc_print_uncaught(void);
/* Optional native process exception registry. Hook returns 1 when handled,
* 0 when unhandled, and -1 when a handler itself threw (Node exit 7). */
extern int (*scr_uncaught_exception_hook)(bool from_promise);
bool scr_exc_handle_uncaught(bool from_promise);
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor);
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor);
/* ── standard library: process + node:fs (scr_lib.c) ─────────────────
* Called once at the top of main, right after scr_init: stashes argc/argv
@@ -3503,6 +3509,7 @@ struct ScrDyn {
* identity while the source reference remains observable. */
ScrDyn *materialized;
ScrDynTypedCast *casts;
bool traced; /* native referent has a cycle header */
} typed_ref;
/* SCR_DYN_PROMISE: the retained promise. The boundary contract: it
* settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a
@@ -3661,6 +3668,11 @@ ScrDyn *scr_dyn_new_typed_ref(
const char *type_key, size_t type_key_len,
ScrDyn *(*materialize)(void *),
void (*commit)(void *, const ScrDyn *));
ScrDyn *scr_dyn_new_typed_ref_traced(
void *ptr, void *(*retain)(void *), void (*release)(void *),
const char *type_key, size_t type_key_len,
ScrDyn *(*materialize)(void *),
void (*commit)(void *, const ScrDyn *));
bool scr_dyn_typed_ref_is(
const ScrDyn *d, const char *type_key, size_t type_key_len);
bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key);
@@ -4531,8 +4543,8 @@ void scr_nticks_teardown(void);
* loop alive, like Node's flowing stdin. */
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
void scr_events_install(void);
void scr_signal_on(double signum, ScrClosure *cb, bool once);
void scr_signal_off(double signum, ScrClosure *cb);
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once);
void scr_signal_off(ScrStr *name, ScrDyn *cb);
void scr_process_on_exit(ScrClosure *cb, void (*fn)(ScrClosure *, double), bool once);
void scr_process_off_exit(ScrClosure *cb);
void scr_run_exit_listeners(double code);
+55
View File
@@ -0,0 +1,55 @@
"use strict";
// An opaque field does not prevent reading or updating the other properties
// of the same native instance through an untyped package boundary.
class Base {
cache = new Map();
width = 40;
child = { value: 1 };
events = [];
get size() {
this.events.push("get");
return this.width * 2;
}
set size(value) {
this.events.push("set");
this.width = value / 2;
}
}
class Renderer extends Base {
height = 24;
get label() { return `size:${this.width}`; }
}
const renderer = new Renderer();
renderer.cache.set("kept", 7);
const view = /** @type {unknown} */ (renderer);
console.log("dimensions", view.width, view.height, view.size, view.label);
view.width = 60;
view.size = 160;
console.log("updates", renderer.width, view.width, view.size, view.label);
console.log("state", renderer.cache.get("kept"), renderer.events.join(","));
console.log("same", view === renderer);
console.log("missing", view.generation === undefined);
view.generation = 1;
const again = /** @type {unknown} */ (renderer);
console.log("shared", again.generation);
again.generation = 2;
console.log("shared update", view.generation);
view.child.value = 9;
console.log("child", renderer.child.value, view.child === again.child);
for (const key of ["width", "height", "size", "label", "generation"]) console.log("computed", key, view[key]);
for (const key of ["width", "generation"]) view[key] = 100;
console.log("computed update", renderer.width, view.generation);
try { view.label = "changed"; } catch (error) { console.log("readonly", error.name, error.message); }
class Data {
first = 1;
second = 2;
}
const data = new Data();
const record = /** @type {unknown} */ (data);
record.extra = 3;
console.log("keys", Object.keys(record).join(","));
console.log("json", JSON.stringify(record));
console.log("descriptor", JSON.stringify(Object.getOwnPropertyDescriptor(record, "extra")));
Object.assign(record, { first: 4, appended: 5 });
console.log("assigned", data.first, record.extra, record.appended, JSON.stringify(record));
@@ -0,0 +1,11 @@
class Context {
id = 1;
}
function createCycle() {
const context = new Context();
const view = /** @type {unknown} */ (context);
view.self = view;
view.nested = { owner: view };
console.log(view.self === view, view.nested.owner === view, context.id);
}
for (let i = 0; i < 3; i++) createCycle();
@@ -0,0 +1,17 @@
class State {
width = 1;
get size() { console.log("get"); return this.width; }
set size(value) { console.log("set", value); this.width = value; }
get failed() { if (this.width > 0) throw new Error("getter"); return 0; }
}
const state = new State();
const view = /** @type {unknown} */ (state);
const key = { toString() { console.log("key"); return "size"; } };
function value() { console.log("value"); return 2; }
view[key] = value();
console.log("read", view[String("size")], state.width);
const throwingKey = { toString() { throw new Error("key conversion"); } };
try { view[throwingKey] = value(); } catch (error) { console.log(error.message); }
try { console.log(view.failed); } catch (error) { console.log(error.message); }
const absent = /** @type {unknown} */ (null);
try { absent[key] = value(); } catch (error) { console.log("absent", error.name); }
+31
View File
@@ -0,0 +1,31 @@
// Named signals use host numbers, deliver their name, and preserve callback identity.
const signal = String("SIGWINCH");
let deliveries = 0;
const keepAlive = setInterval(() => {}, 1000);
const late = () => console.log("late");
const removed = () => console.log("removed snapshot");
const listener = (name, number) => {
console.log("signal", name, number > 0, ++deliveries);
process.off(signal, removed);
if (deliveries === 1) {
process.on(signal, late);
setTimeout(() => process.kill(process.pid, signal), 20);
} else {
process.off(signal, listener);
process.off(signal, late);
clearInterval(keepAlive);
}
};
process.on(signal, listener);
process.once(signal, (name) => console.log("once", name));
process.on(signal, removed);
process.on("SIGBREAK", late);
process.removeListener("SIGBREAK", late);
for (const name of ["SIGKILL", "SIGSTOP"]) {
try {
process.on(name, late);
} catch (error) {
console.log("uncatchable", error.code);
}
}
setTimeout(() => process.kill(process.pid, signal), 20);
@@ -0,0 +1,6 @@
// @exit: 7
process.once("uncaughtException", (error, origin) => {
console.log("handler", error.message, origin);
throw new Error("handler failed");
});
setTimeout(() => { throw new Error("timer"); }, 1);
+20
View File
@@ -0,0 +1,20 @@
let count = 0;
const late = (error) => console.log("late", error.message);
const removed = (error) => console.log("snapshot", error.message);
const handler = (error, origin) => {
console.log("handled", error.message, origin);
process.off("uncaughtException", removed);
if (count === 1) process.once("uncaughtException", late);
process.nextTick(() => console.log("tick", count));
};
process.on("uncaughtException", handler);
process.on("uncaughtException", removed);
process.once("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
const interval = setInterval(() => {
count++;
if (count < 3) throw new Error("interval " + count);
clearInterval(interval);
process.off("uncaughtException", handler);
process.once("uncaughtException", (value, origin) => console.log("primitive", value, origin));
setImmediate(() => { throw "immediate"; });
}, 10);
+7
View File
@@ -0,0 +1,7 @@
process.once("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
process.once("uncaughtException", (error, origin) => {
console.log("handled module", error.message, origin);
setTimeout(() => console.log("resumed"), 10);
});
await Promise.resolve();
throw new Error("module");
@@ -0,0 +1,4 @@
// @exit: 1
// @no-node-shims
process.on("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
throw new Error("unhandled");
@@ -0,0 +1,8 @@
process.once("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
process.once("uncaughtException", (error, origin) => {
console.log("handled rejection", error.message, origin);
process.once("unhandledRejection", () => { throw new Error("listener"); });
process.once("uncaughtException", (error, origin) => console.log("handled listener", error.message, origin));
setTimeout(() => Promise.reject(new Error("next")), 10);
});
Promise.reject(new Error("first"));
+13
View File
@@ -0,0 +1,13 @@
// @no-node-shims
process.once("uncaughtExceptionMonitor", (error, origin) => {
console.log("monitor", error.message, origin);
});
const removed = () => console.log("should not run");
process.on("uncaughtException", removed);
process.off("uncaughtException", removed);
process.once("uncaughtException", (error, origin) => {
console.log("handled", error instanceof Error, error.message, origin);
process.nextTick(() => console.log("handler tick"));
});
setTimeout(() => console.log("survived"), 10);
throw new Error("entry");
+5 -1
View File
@@ -143,7 +143,11 @@ function nodeOracleArgs(file: string): string[] {
? ["--experimental-transform-types", "--disable-warning=ExperimentalWarning"]
: [];
const nodep = wantsNoDeprecation(file) ? ["--no-deprecation"] : [];
return [...transform, ...nodep, "--import", comptimeShim, "--import", islandShim, nodeOracleFile(file)];
// --import makes Node load even a CJS entry through its ESM loader,
// changing an entry throw's uncaughtException origin to unhandledRejection.
const shims = directiveHead(file).includes("// @no-node-shims")
? [] : ["--import", comptimeShim, "--import", islandShim];
return [...transform, ...nodep, ...shims, nodeOracleFile(file)];
}
/** Runs a binary, tolerating an expected nonzero exit (execFile rejects on
+4 -2
View File
@@ -231,8 +231,10 @@ async function runLinuxNode(file: string): Promise<RunResult> {
...(directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l))
? ["--no-deprecation"]
: []),
"--import", inContainer(join(repoRoot, "tests/harness/comptime-shim.mjs")),
"--import", inContainer(join(repoRoot, "tests/harness/island-shim.mjs")),
...(directiveHead(file).includes("// @no-node-shims") ? [] : [
"--import", inContainer(join(repoRoot, "tests/harness/comptime-shim.mjs")),
"--import", inContainer(join(repoRoot, "tests/harness/island-shim.mjs")),
]),
inContainer(nodeOracleFile(file)),
]);
}
+5 -1
View File
@@ -190,7 +190,11 @@ function nodeOracleArgs(file: string): string[] {
? ["--experimental-transform-types", "--disable-warning=ExperimentalWarning"]
: [];
const nodep = wantsNoDeprecation(file) ? ["--no-deprecation"] : [];
return [...transform, ...nodep, "--import", comptimeShim, "--import", islandShim, nodeOracleFile(file)];
// --import makes Node load even a CJS entry through its ESM loader,
// changing an entry throw's uncaughtException origin to unhandledRejection.
const shims = directiveHead(file).includes("// @no-node-shims")
? [] : ["--import", comptimeShim, "--import", islandShim];
return [...transform, ...nodep, ...shims, nodeOracleFile(file)];
}
function programInputs(file: string): string[] {
+4 -1
View File
@@ -85,6 +85,7 @@ const WINDOWS_SKIPS: Record<string, string> = {
// stdout/stderr read "" here where Node types them null (the
// documented spawnSync stance) — invisible on POSIX lanes where these
// spawns succeed, exposed here where every one fails.
"process-named-signals.cjs": "POSIX signal delivery: Windows Node cannot send SIGWINCH or register SIGSTOP",
"1360-spawn-sync.ts": "posix-shaped: every spawn is ENOENT on Windows Node too, exposing the documented spawn-failure \"\"-vs-null stdout stance (1644 covers spawnSync here)",
"1361-spawn-events.ts": "posix-shaped: the unlistened /bin/sh spawn failure crashes both sides, rendered differently (1646 covers spawn events here)",
"1362-spawn-timers.ts": "posix-shaped: the unlistened /bin/sh spawn failure crashes both sides, rendered differently",
@@ -307,8 +308,10 @@ async function runWindowsNode(file: string): Promise<RunResult> {
const nodep = directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l))
? "--no-deprecation "
: "";
const shims = directiveHead(file).includes("// @no-node-shims")
? "" : "--import ./comptime-shim.mjs --import ./island-shim.mjs ";
return runOnBox(
`node ${transform}${nodep}--import ./comptime-shim.mjs --import ./island-shim.mjs ${entry}`,
`node ${transform}${nodep}${shims}${entry}`,
);
}