mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Preserve native class state and handle terminal process events (#571)
- 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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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
|
||||
? [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, ®->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, ®->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, ®->prev, NULL); /* default disposition back */
|
||||
signal(reg->sig, reg->prev);
|
||||
#elif !defined(__wasi__)
|
||||
sigaction(reg->sig, ®->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 = ®->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 ─────────────────────────
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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); }
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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"));
|
||||
@@ -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");
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user