diff --git a/.gitignore b/.gitignore
index 1f9fa24cbc6..c53e9bae00f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ node_modules/
!tests/diagnostics/node-island-function-value/node_modules/
!tests/diagnostics/umd-export-forced-native/node_modules/
!tests/diagnostics/worker-island-value/node_modules/
+!tests/diagnostics/node-island-timer-native-use/node_modules/
!tests/fixtures/commander-calc/node_modules/
!tests/fixtures/npm/node_modules/
!tests/corpus/*/node_modules/
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index b3e4f9f1e89..601b720858b 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -194,6 +194,8 @@ title: "Limitations"
../limitations/runtime-differences-from-node-js/island-packages-that-read-the-machine-when-they-load.mdx
+../limitations/runtime-differences-from-node-js/timers-across-native-code-and-islands.mdx
+
../limitations/runtime-differences-from-node-js/abort-listeners-across-native-code-and-islands.mdx
../limitations/runtime-differences-from-node-js/compile-cache-in-node-islands.mdx
@@ -202,6 +204,8 @@ title: "Limitations"
../limitations/runtime-differences-from-node-js/objects-converted-from-node-islands.mdx
+../limitations/runtime-differences-from-node-js/child-processes-from-node-islands.mdx
+
### Array bounds and runtime errors
../limitations/array-bounds-and-runtime-errors.mdx
diff --git a/docs/content/limitations/runtime-differences-from-node-js/child-processes-from-node-islands.mdx b/docs/content/limitations/runtime-differences-from-node-js/child-processes-from-node-islands.mdx
new file mode 100644
index 00000000000..79c1cda1eed
--- /dev/null
+++ b/docs/content/limitations/runtime-differences-from-node-js/child-processes-from-node-islands.mdx
@@ -0,0 +1,9 @@
+- **Child processes from Node islands:** A `ChildProcess` that JavaScript in a Node island started, for example through a package that wraps `spawn`, and that the program uses at the `ChildProcess` type is the island's own process object: its events arrive in Node.js's order, and its properties, its streams and its listeners are the island object's.
+ The following differs from Node.js.
+ `once` for `spawn`, `exit`, `close` and `error` registers the listener for every emit of the event, which shows only when the event is emitted more than once.
+ When the child has closed, the listeners the program registered on it, on its streams and on its `stdin` are released: they stay registered on the island's object but no longer run, and a listener the program registers afterwards is not registered at all.
+ While the program listens to anything on the child, one more `close` listener is registered on the island's object.
+ A `data` listener that takes `Buffer` chunks throws a `TypeError` when JavaScript in the island set an encoding on that stream, and one that takes strings throws when no encoding is set, where Node.js hands either listener whatever the stream delivers.
+ A write to the child's `stdin` after the child closed reports `ERR_STREAM_DESTROYED` to the write's callback, as under Node.js, and also emits `error` on the stream.
+ An `error` event's error has its `name`, `message` and `code`, and its `errno`, `syscall`, `path` and `spawnargs` read as `undefined`.
+ A listener runs outside the `AsyncLocalStorage` store that was current when the child was started.
diff --git a/docs/content/limitations/runtime-differences-from-node-js/timers-across-native-code-and-islands.mdx b/docs/content/limitations/runtime-differences-from-node-js/timers-across-native-code-and-islands.mdx
new file mode 100644
index 00000000000..9dd0fbf7efb
--- /dev/null
+++ b/docs/content/limitations/runtime-differences-from-node-js/timers-across-native-code-and-islands.mdx
@@ -0,0 +1,4 @@
+- **Timers across native code and islands:** A timer or immediate the program creates is a number in JavaScript that runs in a Node island, as it is in compiled code.
+ The island's `clearTimeout`, `clearInterval` and `clearImmediate` accept that number and clear the program's timer of that kind (`clearTimeout` of an immediate's number clears nothing, as in Node), but `typeof` answers `"number"` there and the value has no `ref`, `unref`, `hasRef`, `refresh` or `close` method.
+ A timer the island creates stays the island's `Timeout` or `Immediate` object: the program clears it with the clear functions, calls its methods and passes it back to the island.
+ In TypeScript, keep such a timer in a `const`; a variable or field typed as a timer holds only the program's own timers, and storing an island's timer in one is a compile-time error.
diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts
index b368aa948ef..8cae5a5853e 100644
--- a/packages/compiler/src/backend/executable-features.ts
+++ b/packages/compiler/src/backend/executable-features.ts
@@ -74,6 +74,9 @@ export function executableLinkFeatures(
...(features.handleProps ? { handleProps: true } : {}),
...(features.childLifecycle ? { childLifecycle: true } : {}),
...(features.processFeatures ? { processFeatures: true } : {}),
+ // A child process an island made, in a native ChildProcess slot
+ // (scr_node_island_child.c): a program with both islands and child_process.
+ ...(features.nodeIsland && features.childProcess ? { islandChild: true } : {}),
...(features.childOptionExt ? { childOptionExt: true } : {}),
...(features.spawnCaptureBytes ? { spawnCaptureBytes: true } : {}),
...(features.childErrorFields ? { childErrorFields: true } : {}),
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index e6a0bfc9915..b32e282bd1f 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -44,6 +44,7 @@ import {
accessorSlotProp,
DYN,
DYN_HANDLE_KINDS,
+ ISLAND_ADOPTED_KINDS,
isDirentShape,
KEY_ORDER_FIELD,
shapeKeyOrders,
@@ -247,7 +248,8 @@ export interface DynHost extends WalkerHost {
declBoundary?(): boolean;
/** The program links Node islands (IrModule.nodeIsland): a value an
* island made is a handle whose members live in Node, and a checked
- * conversion reads them there. */
+ * conversion reads them there; a kind in ISLAND_ADOPTED_KINDS converts
+ * through its island-aware functions. */
nodeIslands?(): boolean;
dynPromiseAdapter(t: IrType): string;
isErrorClass(className: string): boolean;
@@ -1965,6 +1967,15 @@ export class LlDyn {
undefined,
`a checked-dynamic value tested against type ${typeKey(t)}`,
);
+ const adopted = this.islandConversion(t);
+ if (adopted) {
+ // An island's own instance matches too: its runtime unit answers.
+ const matched = B.tmp();
+ this.host.declare(`declare zeroext i1 @${adopted.is}(ptr)`);
+ B.line(`${matched} = call zeroext i1 @${adopted.is}(ptr %d)`);
+ B.terminate(`ret i1 ${matched}`);
+ break;
+ }
const kd = this.kindOf(B, "%d");
const isHandle = B.tmp();
B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`);
@@ -3726,7 +3737,7 @@ export class LlDyn {
const h = DYN_HANDLE_KINDS.get(t.kind);
if (h) {
const r = B.tmp();
- const unbox = DYN_HANDLE_UNBOX_FN[t.kind];
+ const unbox = this.islandConversion(t)?.unbox ?? DYN_HANDLE_UNBOX_FN[t.kind];
if (unbox) {
host.declare(`declare ptr @${unbox}(ptr, ptr, ptr)`);
B.line(`${r} = call ptr @${unbox}(ptr %d, ptr %path, ptr ${want})`);
@@ -3753,6 +3764,14 @@ export class LlDyn {
return name;
}
+ /** The island-aware conversion functions of a kind whose instances made by
+ * a Node island a native slot holds (ISLAND_ADOPTED_KINDS), in a program
+ * that has islands; undefined for every other kind and program. */
+ private islandConversion(t: IrType): { unbox: string; box: string; is: string } | undefined {
+ const adoption = ISLAND_ADOPTED_KINDS.get(t.kind);
+ return adoption?.at === "conversion" && this.host.nodeIslands?.() ? adoption : undefined;
+ }
+
/** `i1`: %d is a handle box with tag `tag`. */
private handleTagMatches(B: BlockBuilder, tag: number): string {
const kd = this.kindOf(B, "%d");
@@ -4591,7 +4610,7 @@ export class LlDyn {
// tag's installed ops.
const h = DYN_HANDLE_KINDS.get(t.kind);
if (h) {
- const box = DYN_HANDLE_BOX_FN[t.kind];
+ const box = this.islandConversion(t)?.box ?? DYN_HANDLE_BOX_FN[t.kind];
const r = B.tmp();
if (box) {
host.declare(`declare ptr @${box}(ptr)`);
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 950cf3c805d..86b652221fc 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -1238,6 +1238,7 @@ export const LIB_FN_SYMS: Record = {
"nodeIsland.superMethod": "scr_node_island_super_method",
"nodeIsland.defineField": "scr_node_island_define_field",
"nodeIsland.promisify": "scr_node_island_promisify",
+ "nodeIsland.clearTimer": "scr_node_island_clear_timer",
"nodeIsland.share": "scr_node_island_share",
"nodeIsland.sharedTdz": "scr_node_island_shared_tdz",
"nodeIsland.classMethodsOf": "scr_node_island_class_methods_of",
@@ -1250,6 +1251,7 @@ export const LIB_FN_SYMS: Record = {
"nodeIsland.setExitCode": "scr_node_island_set_exit_code",
"nodeIsland.exitCode": "scr_node_island_exit_code",
"nodeIsland.hasExitCode": "scr_node_island_has_exit_code",
+ "nodeIsland.chdir": "scr_node_island_chdir",
"dyn.keySet": "scr_dyn_key_set",
"dyn.fnOwnSet": "scr_dyn_fn_own_set",
"dyn.keySetComputed": "scr_dyn_key_set_computed",
diff --git a/packages/compiler/src/backend/llvm/runtime-registries.test.ts b/packages/compiler/src/backend/llvm/runtime-registries.test.ts
index 34c8bacedbf..1db0887dd26 100644
--- a/packages/compiler/src/backend/llvm/runtime-registries.test.ts
+++ b/packages/compiler/src/backend/llvm/runtime-registries.test.ts
@@ -10,7 +10,15 @@ import {
} from "./common.js";
import { DYN_KIND, MESSAGE_KIND } from "./dyn.js";
import { mapKeyKindNum } from "./shapes.js";
-import { BOOL, DYN, F64, STRING, type IrType } from "../../ir/ir.js";
+import {
+ BOOL,
+ DYN,
+ DYN_HANDLE_KINDS,
+ F64,
+ ISLAND_ADOPTED_KINDS,
+ STRING,
+ type IrType,
+} from "../../ir/ir.js";
// The backend writes the numbers of several runtime enums and flag sets
// into the IR it emits, and the IR layer names the methods a runtime
@@ -186,3 +194,60 @@ test("mapKeyKindNum matches ScrMapKeyKind", () => {
expect(mapKeyKindNum(union, [record, record])).toBe(kinds.get("SCR_MAP_KEY_UNION_REF"));
expect(mapKeyKindNum(union, [STRING, F64])).toBe(kinds.get("SCR_MAP_KEY_UNION_VALUE"));
});
+
+test("ISLAND_ADOPTED_KINDS names conversions the runtime defines", () => {
+ // A kind whose island-made instances a native slot holds converts through
+ // its unit's own functions in a program with islands (dyn.ts
+ // islandConversion). The unit is named after the kind's handle.
+ for (const [kind, adoption] of ISLAND_ADOPTED_KINDS) {
+ expect(DYN_HANDLE_KINDS.has(kind), kind).toBe(true);
+ if (adoption.at !== "conversion") continue;
+ const unit = source(`scr_node_island_${kind}.c`);
+ const defined = (signature: RegExp, name: string) =>
+ expect(signature.test(unit), `${name} in scr_node_island_${kind}.c`).toBe(true);
+ defined(
+ new RegExp(
+ `^\\w+ \\*${adoption.unbox}\\(const ScrDyn \\*\\w+, const ScrDynPath \\*\\w+, const char \\*\\w+\\) \\{`,
+ "m",
+ ),
+ adoption.unbox,
+ );
+ defined(new RegExp(`^ScrDyn \\*${adoption.box}\\(\\w+ \\*\\w+\\) \\{`, "m"), adoption.box);
+ defined(new RegExp(`^bool ${adoption.is}\\(const ScrDyn \\*\\w+\\) \\{`, "m"), adoption.is);
+ }
+});
+
+test("every entry point that takes a child handle hands an island's to the island", () => {
+ // A ChildProcess slot can hold a child a Node island made
+ // (scr_node_island_child.c). It has no native state, so an entry point that
+ // read its fields would answer for a child that does not exist: each one
+ // that takes a child, one of its streams or its writer starts with the
+ // island guard. Reference counting is the same for both.
+ const counted =
+ /^scr_child(?:_stream|_writer)?_(?:retain|release)_v$|^scr_child(?:_stream|_writer)?_retain$/;
+ const made = /^scr_child(?:_stream|_writer)?_island_new$/;
+ for (const file of ["scr_child.c", "scr_child_lifecycle.c"]) {
+ const text = source(file);
+ // scr_child.c's Windows arm has no islands; the POSIX arm and the code
+ // both arms share follow it.
+ const posix =
+ file === "scr_child.c" ? text.slice(text.search(/#else\s*\n\s*#include /)) : text;
+ expect(posix.length, file).toBeGreaterThan(1000);
+ const entry =
+ /^(?!static)[\w ]+[ *](scr_child\w*)\(Scr(?:Child|ChildStream|ChildWriter) \*\w+[^)]*\) \{/gm;
+ const unguarded: string[] = [];
+ let seen = 0;
+ for (const match of posix.matchAll(entry)) {
+ const name = match[1]!;
+ if (counted.test(name) || made.test(name)) continue;
+ seen++;
+ const start = match.index! + match[0].length;
+ const body = posix.slice(start, posix.indexOf("\n}", start) + 1);
+ const oneLine = posix.slice(start, posix.indexOf("\n", start));
+ if (!(oneLine.trimEnd().endsWith("}") ? oneLine : body).includes("SCR_IF_ISLAND("))
+ unguarded.push(name);
+ }
+ expect(seen, file).toBeGreaterThan(file === "scr_child.c" ? 35 : 5);
+ expect(unguarded, file).toEqual([]);
+ }
+});
diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts
index eb836232699..facc944cc20 100644
--- a/packages/compiler/src/backend/native-link-info-core.ts
+++ b/packages/compiler/src/backend/native-link-info-core.ts
@@ -132,6 +132,10 @@ export interface NativeLinkFeatures {
/** process.features read as a value links scr_process_features.c (the
* stable flags object); absent (false) otherwise. */
processFeatures?: boolean;
+ /** A program with both Node islands and child_process links
+ * scr_node_island_child.c (a child process an island made, in a native
+ * ChildProcess slot); absent (false) otherwise. */
+ islandChild?: boolean;
/** The spawnSync and exec/execFile-callback `cwd`/`input`/`shell`
* entries: selects the SCR_CHILD_CP_OPTIONS build of scr_child.c.
* Absent (false) otherwise. */
diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts
index cba7c4fb6d9..34dd65ee43c 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -152,6 +152,9 @@ export interface CcOptions {
childLifecycle?: boolean;
/** process.features as a value links scr_process_features.c. */
processFeatures?: boolean;
+ /** Node islands and child_process together: compiles
+ * scr_node_island_child.c. */
+ islandChild?: boolean;
/** The spawnSync and exec/execFile-callback `cwd`/`input`/`shell`
* entries: the SCR_CHILD_CP_OPTIONS build of scr_child.c. */
childOptionExt?: boolean;
diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts
index 889e8ebcd62..fd02bdfeea8 100644
--- a/packages/compiler/src/backend/native/executable.ts
+++ b/packages/compiler/src/backend/native/executable.ts
@@ -513,6 +513,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
(u) => u.opts.nodeIsland === true && (u.nativeFetch || u.opts.abortSignal === true),
"scr_node_island_abort.c",
),
+ // A child process an island made, in a native ChildProcess slot: a
+ // program with both islands and child_process.
+ unit((u) => u.opts.islandChild === true, "scr_node_island_child.c"),
// Readable.fromWeb reads a web ReadableStream through its reader.
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
// The dynamic global object's answer for the Node globals the native
diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts
index a8db917a7d2..454ea72bfeb 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -96,6 +96,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
handleProps: boolean;
childLifecycle: boolean;
processFeatures: boolean;
+ islandChild: boolean;
childOptionExt: boolean;
spawnCaptureBytes: boolean;
childErrorFields: boolean;
@@ -383,6 +384,7 @@ export function effectiveRuntimeFeatures(
handleProps: features.handleProps === true,
childLifecycle: features.childLifecycle === true,
processFeatures: features.processFeatures === true,
+ islandChild: features.islandChild === true,
childOptionExt: features.childOptionExt === true,
spawnCaptureBytes: features.spawnCaptureBytes === true,
childErrorFields: features.childErrorFields === true,
diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts
index 8e574710814..9b9df3e29c2 100644
--- a/packages/compiler/src/executable/executable-cache.ts
+++ b/packages/compiler/src/executable/executable-cache.ts
@@ -113,6 +113,7 @@ export interface EarlyExecutableNativeFeatures {
childLifecycle?: boolean;
/** process.features as a value: scr_process_features.c. */
processFeatures?: boolean;
+ islandChild?: boolean;
childOptionExt?: boolean;
spawnCaptureBytes?: boolean;
declAdopt?: boolean;
diff --git a/packages/compiler/src/frontend/lowering/island-process-state.test.ts b/packages/compiler/src/frontend/lowering/island-process-state.test.ts
index ef268c00815..d9c8ef925d4 100644
--- a/packages/compiler/src/frontend/lowering/island-process-state.test.ts
+++ b/packages/compiler/src/frontend/lowering/island-process-state.test.ts
@@ -38,6 +38,13 @@ const calls = (): (IrExpr & { kind: "libCall" })[] => [
{ kind: "libCall", fn: "process.currentExitCode", args: [], type: F64, loc },
{ kind: "libCall", fn: "process.hasExitCode", args: [], type: BOOL, loc },
{ kind: "libCall", fn: "process.cwd", args: [], type: { kind: "string" }, loc },
+ {
+ kind: "libCall",
+ fn: "process.chdir",
+ args: [{ kind: "strLit", value: "..", type: { kind: "string" }, loc }],
+ type: VOID,
+ loc,
+ },
];
const body = (exprs: IrExpr[]): IrStmt[] => exprs.map((expr) => ({ kind: "exprStmt", expr, loc }));
@@ -52,6 +59,7 @@ test("a program with Node islands reads and writes process.exitCode through the
"nodeIsland.exitCode",
"nodeIsland.hasExitCode",
"process.cwd",
+ "nodeIsland.chdir",
]);
});
@@ -66,5 +74,6 @@ test("a program without Node islands keeps the runtime's exit-code calls", () =>
"process.currentExitCode",
"process.hasExitCode",
"process.cwd",
+ "process.chdir",
]);
});
diff --git a/packages/compiler/src/frontend/lowering/island-process-state.ts b/packages/compiler/src/frontend/lowering/island-process-state.ts
index e3cb6628c75..9fcf7b93cde 100644
--- a/packages/compiler/src/frontend/lowering/island-process-state.ts
+++ b/packages/compiler/src/frontend/lowering/island-process-state.ts
@@ -4,7 +4,8 @@
* writes directly, and the program's native reads and writes go to it
* through the island unit (scr_node_island_events.c). Until Node is loaded
* the island unit answers from the runtime's own value, as the calls it
- * replaces do. */
+ * replaces do. `process.chdir` goes the same way: the directory is the
+ * process's, and Node is told to drop its cached `process.cwd()`. */
import type { IrExpr, IrFunction, IrLibFn } from "../../ir/ir.js";
import { everyStmt } from "../../ir/traverse.js";
import type { Lowerer } from "./lowerer.js";
@@ -17,10 +18,14 @@ const EXIT_CODE_CALLS: ReadonlyMap = new Map
["process.setExitCodeTracked", "nodeIsland.setExitCode"],
["process.currentExitCode", "nodeIsland.exitCode"],
["process.hasExitCode", "nodeIsland.hasExitCode"],
+ // The working directory is the process's too, and Node caches its answer
+ // to process.cwd(): the program's change tells Node to read it again.
+ ["process.chdir", "nodeIsland.chdir"],
]);
-/** Routes the exit-code calls through the island unit, over the final
- * functions, in place. Programs without islands are left as they are. */
+/** Routes the exit-code calls and process.chdir through the island unit,
+ * over the final functions, in place. Programs without islands are left as
+ * they are. */
export function shareIslandProcessState(lowerer: Lowerer, functions: IrFunction[]): void {
if (!lowerer.usesNodeIsland) return;
const visitor = {
diff --git a/packages/compiler/src/frontend/lowering/lower-node-island.ts b/packages/compiler/src/frontend/lowering/lower-node-island.ts
index b309631bc03..9464f40f977 100644
--- a/packages/compiler/src/frontend/lowering/lower-node-island.ts
+++ b/packages/compiler/src/frontend/lowering/lower-node-island.ts
@@ -931,6 +931,17 @@ function islandValueAt(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrEx
return raw;
}
if (mapped.kind === "void") return raw;
+ // A timer the island created stays the island's Timeout or Immediate (the
+ // program's own handles are numbers): the program clears it, calls its
+ // methods and passes it back through the handle.
+ if (islandTimerType(lowerer, tsType)) {
+ if (handleUseOk(lowerer, node)) return raw;
+ boundaryFail(
+ lowerer,
+ `an island value of type '${lowerer.checker.typeToString(tsType)}' used as a native value (a timer created by island code stays the island's object: keep it in a const, clear it, call its methods or pass it back to the island)`,
+ node,
+ );
+ }
// An island promise is the same promise on both sides (the runtime keeps
// one ScrPromise per V8 promise): PromiseResolve of the dyn value, whose
// settlement value meets the declared type when it is awaited.
@@ -1258,12 +1269,61 @@ function isForeignExpr(lowerer: Lowerer, node: ts.Expression): boolean {
return isIslandHandleExpr(lowerer, inner);
}
+/** Whether `type` is the Timeout or Immediate of the standard library or
+ * @types/node (nullish arms aside). A timer island code created is the
+ * island's object: the program's own timer handles are numbers, which it is
+ * not, so it stays a handle the program clears, calls methods on and passes
+ * back. */
+function islandTimerType(lowerer: Lowerer, type: ts.Type): boolean {
+ if (type.isUnionType()) {
+ const parts = ts
+ .constituentTypes(type)
+ .filter(
+ (t) => (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0,
+ );
+ return parts.length > 0 && parts.every((t) => islandTimerType(lowerer, t));
+ }
+ const symbol = type.getAliasSymbol() ?? type.getSymbol();
+ if (symbol === undefined) return false;
+ const name = ts.symbolName(symbol);
+ if (name !== "Timeout" && name !== "Immediate") return false;
+ return symbol.declarations.some(
+ (d) =>
+ (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
+ );
+}
+
+/** Whether `callee` names clearTimeout, clearInterval or clearImmediate: the
+ * globals or the node:timers functions. Their argument may be an island's
+ * timer, which Node's own function clears (lower-timers.ts). */
+function isTimerClearCallee(lowerer: Lowerer, callee: ts.Expression): boolean {
+ const c = withoutParenthesesTypeWrappers(callee);
+ const clear = (name: string): boolean =>
+ name === "clearTimeout" || name === "clearInterval" || name === "clearImmediate";
+ if (ts.isPropertyAccessExpression(c)) {
+ const member = lowerer.builtinMemberOf(c);
+ return member !== null && member.module === "timers" && clear(member.member);
+ }
+ if (!ts.isIdentifier(c)) return false;
+ const imported = lowerer.builtinImportOf(c);
+ if (imported) return imported.module === "timers" && clear(imported.member);
+ if (!clear(c.text)) return false;
+ const declarations = lowerer.checker.getSymbolAtLocation(c)?.declarations ?? [];
+ return (
+ declarations.length > 0 &&
+ declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
+ );
+}
+
/** Whether a value of TypeScript type `type` coming out of an island is
* an object the island does not declare and the program cannot take by
- * reference natively (arrays, tuples, Maps, Sets): it stays a handle. */
+ * reference natively (arrays, tuples, Maps, Sets, timers): it stays a
+ * handle. */
function undeclaredHandleType(lowerer: Lowerer, type: ts.Type): boolean {
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return false;
if (isForeignType(lowerer.nodeIslands!, type, lowerer.checker)) return false;
+ if (islandTimerType(lowerer, type)) return true;
const mapped = lowerer.mapTypeOf(type);
// No native representation at all: the island's object is the only one
// there is. A function object keeps its own rule (fromIsland).
@@ -1584,8 +1644,16 @@ function handleUseOk(lowerer: Lowerer, node: ts.Expression): boolean {
return isForeignExpr(lowerer, c.expression);
return isForeignExpr(lowerer, c);
};
- if (ts.isCallExpression(p) || ts.isNewExpression(p))
- return p.expression === n || islandCallee(p.expression);
+ if (ts.isCallExpression(p) || ts.isNewExpression(p)) {
+ if (p.expression === n || islandCallee(p.expression)) return true;
+ // clearTimeout(islandTimer): Node's own clear function clears it.
+ return (
+ ts.isCallExpression(p) &&
+ p.arguments.nodes[0] === n &&
+ islandTimerType(lowerer, lowerer.typeOf(node)) &&
+ isTimerClearCallee(lowerer, p.expression)
+ );
+ }
if (ts.isSpreadElement(p)) {
const q = p.parent;
if (ts.isArrayLiteralExpression(q)) return true;
diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts
index 832706598c2..8b9a907f1a1 100644
--- a/packages/compiler/src/frontend/lowering/lower-timers.ts
+++ b/packages/compiler/src/frontend/lowering/lower-timers.ts
@@ -3,6 +3,7 @@ import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
import * as ts from "../ts7/adapter.js";
import { withoutParentheses } from "../outer-expressions.js";
import { isJsSourceFile } from "../program.js";
+import { programImportsNodeApi } from "../node-api-import.js";
import {
BOOL,
canBoxFuncIntoDyn,
@@ -656,7 +657,7 @@ function lowerTimerCall(
if (expr.arguments.nodes.length !== 1) {
lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr);
}
- const handle = optionalTimerHandle(lowerer, untypedTimerHandle(lowerer, expr, loc), loc);
+ const handle = optionalTimerHandle(lowerer, untypedTimerHandle(lowerer, expr, 0, loc), loc);
if (handle.type.kind !== "f64") {
lowerer.noLowering(
`clearTimeout of '${lowerer.fmt(handle.type)}' handles`,
@@ -672,7 +673,7 @@ function lowerTimerCall(
if (expr.arguments.nodes.length !== 1) {
lowerer.noLowering(`clearInterval with ${expr.arguments.nodes.length} arguments`, expr);
}
- const handle = optionalTimerHandle(lowerer, untypedTimerHandle(lowerer, expr, loc), loc);
+ const handle = optionalTimerHandle(lowerer, untypedTimerHandle(lowerer, expr, 1, loc), loc);
if (handle.type.kind !== "f64") {
lowerer.noLowering(
`clearInterval of '${lowerer.fmt(handle.type)}' handles`,
@@ -683,8 +684,9 @@ function lowerTimerCall(
return { kind: "libCall", fn: "timers.clearInterval", args: [handle], type: VOID, loc };
}
// setImmediate/clearImmediate: Node's check-phase pair. The handle is
- // the f64 immediate id (its own space — clearTimeout of an Immediate
- // no-ops, like Node); the callback adapts like setTimeout's.
+ // the f64 immediate id, drawn from the timer sequence so its number
+ // never names a timeout (clearTimeout of an Immediate no-ops, like
+ // Node); the callback adapts like setTimeout's.
if (member === "setImmediate") {
if (expr.arguments.nodes.length === 0) {
lowerer.noLowering(
@@ -737,7 +739,7 @@ function lowerTimerCall(
if (expr.arguments.nodes.length !== 1) {
lowerer.noLowering(`clearImmediate with ${expr.arguments.nodes.length} arguments`, expr);
}
- const handle = optionalTimerHandle(lowerer, untypedTimerHandle(lowerer, expr, loc), loc);
+ const handle = optionalTimerHandle(lowerer, untypedTimerHandle(lowerer, expr, 2, loc), loc);
if (handle.type.kind !== "f64") {
lowerer.noLowering(
`clearImmediate of '${lowerer.fmt(handle.type)}' handles`,
@@ -753,12 +755,29 @@ function lowerTimerCall(
/** The handle argument of a clear call. A checked-dynamic handle in
* JavaScript (a timer callback's `this`, an untyped binding) clears when it
* holds a number, the handle's representation; any other value clears
- * nothing, as Node's clear functions ignore values that are not timers. */
-function untypedTimerHandle(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
+ * nothing, as Node's clear functions ignore values that are not timers.
+ *
+ * In a program with Node islands the value may be a timer island code
+ * created (an island's Timeout or Immediate object): Node's own clear
+ * function clears it (`which` is 0 for clearTimeout, 1 for clearInterval, 2
+ * for clearImmediate). Such a program takes a checked-dynamic handle in
+ * TypeScript too, where an island's values are untyped. */
+function untypedTimerHandle(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ which: 0 | 1 | 2,
+ loc: SrcLoc,
+): IrExpr {
const handle = lowerer.lowerExpr(expr.arguments.nodes[0]!);
- if (handle.type.kind !== "dyn" || !isJsSourceFile(ts.getSourceFileOfNode(expr))) return handle;
+ if (handle.type.kind !== "dyn") return handle;
+ const islands =
+ lowerer.nodeIslands !== null ||
+ (lowerer.importsNodeApi ??= programImportsNodeApi(lowerer.program));
+ if (!islands && !isJsSourceFile(ts.getSourceFileOfNode(expr))) return handle;
const stored = lowerer.declareHiddenLocal("%timer.clear", DYN);
const ref = varRef(stored.id, DYN, loc);
+ const zero = numLit(0, loc);
+ if (islands) lowerer.usesNodeIsland = true;
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: stored.id, init: handle, loc }],
@@ -766,7 +785,27 @@ function untypedTimerHandle(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcL
kind: "ternary",
cond: { kind: "dynTest", test: "number", value: ref, type: BOOL, loc },
then: lowerer.coerceInto(expr.arguments.nodes[0]!, ref, F64),
- else_: numLit(0, loc),
+ else_: islands
+ ? {
+ kind: "seqExpr",
+ stmts: [
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "nodeIsland.clearTimer",
+ args: [ref, numLit(which, loc)],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ result: zero,
+ type: F64,
+ loc,
+ }
+ : zero,
type: F64,
loc,
},
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 771f0c69c3f..4545cdcf0e3 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -4397,6 +4397,10 @@ export class Lowerer {
/** Which member calls on records pass the record as the receiver
* (memberCallReceivers), computed on first use. */
memberReceivers: MemberCallReceivers | undefined = undefined;
+ /** Whether the program imports @scriptc/node (programImportsNodeApi), so
+ * a checked-dynamic value may be a Node island value; computed on first
+ * use. */
+ importsNodeApi: boolean | undefined = undefined;
/** Builtin-module FUNCTIONS admitted as values by their surface-table
* entries. The resolved runtime function plus ABI is the identity key,
* so named imports, namespace reads, CommonJS bindings, and alias chains
@@ -5676,6 +5680,7 @@ export class Lowerer {
"mentionsProcessVersions",
"createsWrappers",
"memberReceivers",
+ "importsNodeApi",
"stringIndexBoundsCache",
"functionLocalSubclasses",
"socketEncodingReachable",
diff --git a/packages/compiler/src/frontend/node-api-import.ts b/packages/compiler/src/frontend/node-api-import.ts
index 18042d506c0..b3b61a0a28b 100644
--- a/packages/compiler/src/frontend/node-api-import.ts
+++ b/packages/compiler/src/frontend/node-api-import.ts
@@ -63,6 +63,20 @@ export function nodeApiCalleeOf(program: ts.Program, callee: ts.Expression): Nod
return isNodeApiNamespace(program, callee.expression) ? member : null;
}
+/** Whether any source file of the program imports @scriptc/node, so its
+ * checked-dynamic values may be Node island values (a requireModule result
+ * or anything reached from one). The lowerer keeps the answer
+ * (Lowerer.importsNodeApi). */
+export function programImportsNodeApi(program: ts.Program): boolean {
+ return program
+ .getSourceFiles()
+ .some(
+ (sf) =>
+ !sf.isDeclarationFile &&
+ sf.statements.nodes.some((stmt) => moduleSpecifierText(stmt) === NODE_API_MODULE),
+ );
+}
+
/** Whether an expression evaluates to a value owned by a Node island: a
* requireModule(...) result or anything reached from one through member
* reads, element reads, destructuring or a const alias (`const { Point } =
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 431fea89b84..16f1ff426ef 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -917,6 +917,7 @@ async function compileExecutableNative(
...(features.handleProps ? { handleProps: true } : {}),
...(features.childLifecycle ? { childLifecycle: true } : {}),
...(features.processFeatures ? { processFeatures: true } : {}),
+ ...(features.islandChild ? { islandChild: true } : {}),
...(features.childOptionExt ? { childOptionExt: true } : {}),
...(features.spawnCaptureBytes ? { spawnCaptureBytes: true } : {}),
...(features.childErrorFields ? { childErrorFields: true } : {}),
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 7e584058cd4..7c7814761ef 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -38,6 +38,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"nodeIsland.promiseResolve",
// A code that is not a safe integer throws, as process.setExitCode does.
"nodeIsland.setExitCode",
+ // A directory the process cannot enter throws, as process.chdir does.
+ "nodeIsland.chdir",
+ // Node's own clearTimeout, clearInterval or clearImmediate on an island
+ // value, which is island JavaScript: a proxy's trap can throw there.
+ "nodeIsland.clearTimer",
// Both read Node's process.exitCode through an island helper once Node is
// loaded, which is island JavaScript (the property can be redefined
// there). The runtime drops what that read throws today and answers its
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 5c05ec85c8b..f5afff17147 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -242,6 +242,10 @@ function buildLibFnSignatures() {
"nodeIsland.superMethod": { argTypes: [DYN, DYN, STRING, DYN], result: DYN },
"nodeIsland.defineField": { argTypes: [DYN, STRING, DYN], result: VOID },
"nodeIsland.promisify": { argTypes: [DYN], result: DYN },
+ /** clearTimeout (0), clearInterval (1) or clearImmediate (2) of a
+ * checked-dynamic value that is not a number: an island's timer is
+ * cleared by Node's own function, any other value is ignored. */
+ "nodeIsland.clearTimer": { argTypes: [DYN, F64], result: VOID },
/** NI-6 shared modules: registers a module the program shares with its
* islands (real path, exports JSON, accessor); the TDZ sentinel an
* accessor answers; the method table and constructor helper of an
@@ -271,6 +275,10 @@ function buildLibFnSignatures() {
"nodeIsland.setExitCode": { argTypes: [F64], result: VOID },
"nodeIsland.exitCode": { argTypes: [], result: F64 },
"nodeIsland.hasExitCode": { argTypes: [], result: BOOL },
+ /** process.chdir in a program with Node islands: the runtime's own
+ * change, then Node drops its cached process.cwd()
+ * (island-process-state.ts). */
+ "nodeIsland.chdir": { argTypes: [STRING], result: VOID },
/** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a
* checked-dynamic object (args: receiver, key string, value — all
* borrowed; the runtime copies the key and retains the value in). An
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 945d3c4c709..c1553f47f36 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -4662,6 +4662,50 @@ export const DYN_HANDLE_KINDS: ReadonlyMap
["cryptoHmac", { tag: "SCR_DYNH_HMAC", cls: "Hmac" }],
]);
+/** How a Node island's own instance of a native platform class comes to sit in
+ * a native slot of the class's kind.
+ *
+ * `at: "conversion"`: where a dynamic value enters the slot (the checked
+ * conversion). The value stays the island's object everywhere else, and
+ * converting the slot's value back to a dynamic one answers that object. The
+ * runtime functions named here do the three conversions in a program with
+ * Node islands; they link only into programs that have both islands and the
+ * kind's unit.
+ *
+ * `at: "import"`: where the island's object enters native code. It is the
+ * native object from then on, also in untyped code. */
+export type IslandAdoption =
+ | {
+ at: "conversion";
+ /** `ptr (ptr dyn, ptr path, ptr want)`: the slot's value (+1), or the conversion's TypeError. */
+ unbox: string;
+ /** `ptr (ptr value)`: the dynamic value (+1). */
+ box: string;
+ /** `i1 (ptr dyn)`: the value is one (a union's arm test). */
+ is: string;
+ }
+ | { at: "import" };
+
+/** The IR kinds whose instances made by a Node island a native slot of that
+ * kind holds, and how. Every other kind in DYN_HANDLE_KINDS, and Date, RegExp,
+ * URL and the other native objects with their own state, hold only native
+ * instances: an island's is a handle where it is used through its members and
+ * the conversion's TypeError (or a build-time refusal) in a native slot. */
+export const ISLAND_ADOPTED_KINDS: ReadonlyMap = new Map<
+ IrType["kind"],
+ IslandAdoption
+>([
+ [
+ "child",
+ {
+ at: "conversion",
+ unbox: "scr_ni_child_unbox",
+ box: "scr_ni_child_box",
+ is: "scr_ni_child_is",
+ },
+ ],
+]);
+
/** A class value that crosses an `unknown` slot as a compiler-owned typed
* reference. %Error keeps its dedicated error encoding; every other class
* preserves the original object identity and exposes a materialized own-field
diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts
index 7000aef0cb0..b052870c9a8 100644
--- a/packages/compiler/src/native/sanitizer.test.ts
+++ b/packages/compiler/src/native/sanitizer.test.ts
@@ -19,6 +19,7 @@ const features = {
handleProps: false,
childLifecycle: false,
processFeatures: false,
+ islandChild: false,
childOptionExt: false,
spawnCaptureBytes: false,
childErrorFields: false,
diff --git a/packages/compiler/src/program-facts.ts b/packages/compiler/src/program-facts.ts
index 66037e42845..8fcce4cdbca 100644
--- a/packages/compiler/src/program-facts.ts
+++ b/packages/compiler/src/program-facts.ts
@@ -525,6 +525,20 @@ export const PROGRAM_FACTS: readonly ProgramFact[] = [
tracked: "FOLLOWUPS 2026-11-07 (holed-array-generics)",
lowererFields: [],
},
+ {
+ name: "imports-node-api",
+ file: `${SRC}/frontend/node-api-import.ts`,
+ symbol: "programImportsNodeApi",
+ source: "syntax",
+ scope: "program",
+ setBy: "an import of `@scriptc/node` in any source file of the program",
+ gates:
+ "clearTimeout, clearInterval and clearImmediate of a checked-dynamic handle also ask the Node island unit to clear an island's timer, and take such a handle in TypeScript",
+ narrower:
+ "the handle, at run time: a requireModule result and everything reached from it is untyped, so no site knows whether its value came from an island",
+ tracked: "pi-decisions DECISIONS island-timers-iteration 2026-11-07",
+ lowererFields: ["importsNodeApi"],
+ },
];
/** Why a whole-program scan decides no registered fact.
@@ -805,6 +819,7 @@ export const PROGRAM_SCANS: readonly ProgramScan[] = [
"key",
"a digest of the files a reused unit does not own",
),
+ decides("frontend/node-api-import.ts#programImportsNodeApi", 1, "imports-node-api"),
scan(
"frontend/pipeline.ts#addDynamicImportGraph",
1,
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9880-node-island-child-adopted/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9880-node-island-child-adopted/main.ts.json
new file mode 100644
index 00000000000..8f9ce9deb81
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9880-node-island-child-adopted/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9880-node-island-child-adopted/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9881-node-island-child-typed-crossings/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9881-node-island-child-typed-crossings/main.ts.json
new file mode 100644
index 00000000000..187cb2fe548
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9881-node-island-child-typed-crossings/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9881-node-island-child-typed-crossings/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9882-node-island-child-events/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9882-node-island-child-events/main.ts.json
new file mode 100644
index 00000000000..1d0d4d4273c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9882-node-island-child-events/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9882-node-island-child-events/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9883-node-island-child-spawn-failure/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9883-node-island-child-spawn-failure/main.ts.json
new file mode 100644
index 00000000000..605870b1d96
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9883-node-island-child-spawn-failure/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9883-node-island-child-spawn-failure/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9884-node-island-child-identity/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9884-node-island-child-identity/main.ts.json
new file mode 100644
index 00000000000..39940452223
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9884-node-island-child-identity/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9884-node-island-child-identity/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9885-node-island-child-outlives-reference/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9885-node-island-child-outlives-reference/main.ts.json
new file mode 100644
index 00000000000..28e9dc3f66c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9885-node-island-child-outlives-reference/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9885-node-island-child-outlives-reference/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9886-node-island-child-stdin/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9886-node-island-child-stdin/main.ts.json
new file mode 100644
index 00000000000..fcb880574ed
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9886-node-island-child-stdin/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9886-node-island-child-stdin/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9887-node-island-child-unhandled-error/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9887-node-island-child-unhandled-error/main.ts.json
new file mode 100644
index 00000000000..5f7c67b0ddc
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9887-node-island-child-unhandled-error/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9887-node-island-child-unhandled-error/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9888-node-island-child-general-streams/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9888-node-island-child-general-streams/main.ts.json
new file mode 100644
index 00000000000..37e467c8dfe
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9888-node-island-child-general-streams/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9888-node-island-child-general-streams/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9889-node-island-child-ipc/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9889-node-island-child-ipc/main.ts.json
new file mode 100644
index 00000000000..5ef63cbdad7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9889-node-island-child-ipc/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9889-node-island-child-ipc/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9890-node-island-child-off-during-emit/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9890-node-island-child-off-during-emit/main.ts.json
new file mode 100644
index 00000000000..5f0fc8f34e0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9890-node-island-child-off-during-emit/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9890-node-island-child-off-during-emit/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9894-node-island-process-cwd/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9894-node-island-process-cwd/main.ts.json
new file mode 100644
index 00000000000..2de4546bf5f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9894-node-island-process-cwd/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9894-node-island-process-cwd/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9895-node-island-child-close-rest-listener/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9895-node-island-child-close-rest-listener/main.ts.json
new file mode 100644
index 00000000000..dc03966289f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9895-node-island-child-close-rest-listener/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9895-node-island-child-close-rest-listener/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9896-node-island-child-stream-pause/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9896-node-island-child-stream-pause/main.ts.json
new file mode 100644
index 00000000000..b92ebd03c28
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/9896-node-island-child-stream-pause/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/9896-node-island-child-stream-pause/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-clear-across/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-clear-across/main.mjs.json
new file mode 100644
index 00000000000..cb28ec6e998
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-clear-across/main.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-clear-across/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-cross-kind-clears/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-cross-kind-clears/main.mjs.json
new file mode 100644
index 00000000000..68e79cb24f3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-cross-kind-clears/main.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-cross-kind-clears/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-island-clears/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-island-clears/main.mjs.json
new file mode 100644
index 00000000000..f994494cf92
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-island-clears/main.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-island-clears/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-methods/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-methods/main.mjs.json
new file mode 100644
index 00000000000..fcc511b88f2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-methods/main.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-methods/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-native-clears/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-native-clears/main.mjs.json
new file mode 100644
index 00000000000..3c55f3522d2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-native-clears/main.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-native-clears/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-promises/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-promises/main.mjs.json
new file mode 100644
index 00000000000..8c0232a0456
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-promises/main.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-promises/main.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-typed-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-typed-package/main.ts.json
new file mode 100644
index 00000000000..a4dac803e92
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-typed-package/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-typed-package/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-typescript/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-typescript/main.ts.json
new file mode 100644
index 00000000000..9ebd5f14a6c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/node-island-timers-typescript/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/node-island-timers-typescript/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/timers-cross-kind-clears.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/timers-cross-kind-clears.ts.json
new file mode 100644
index 00000000000..7ec2c648ee3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/timers-cross-kind-clears.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/timers-cross-kind-clears.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-island-timer-native-use/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-island-timer-native-use/main.ts.json
new file mode 100644
index 00000000000..aabdaee2ce6
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-island-timer-native-use/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/node-island-timer-native-use/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs
index c1f6fe1879b..f686a21b768 100644
--- a/packages/runtime-pack-common/runtime-pack-matrix.mjs
+++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs
@@ -306,6 +306,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [
// native signals (the signal unit or the fetch unit).
["scr_node_island_abort.c", { all: ["nodeIsland"], any: ["abortSignal", "nativeFetch"] }],
["scr_node_island_events.c", "nodeIsland"],
+ // A child process an island made, in a native ChildProcess slot: programs
+ // with both islands and child_process. An object on the link line leaves
+ // its file symbol in the executable, so no other program gets it.
+ ["scr_node_island_child.c", "islandChild"],
["scr_stream_from_web.c", all("stream", "nativeFetch")],
// The dynamic global object's answer for the Node globals the native runtime
// does not implement (host-globals.ts): the compiler hands over its derived
@@ -700,6 +704,27 @@ export function variantsFor(source) {
],
])
variants.push({ id, when, defines });
+ // A program with Node islands can hold a child process an island made in
+ // a ChildProcess slot (scr_node_island_child.c): in the SCR_NODE_ISLANDS
+ // build every entry point hands such a child to the island's object.
+ // Every other program keeps the builds above, byte for byte. The selector
+ // takes the variant with the most conditions, so each build above has its
+ // counterpart.
+ for (const { id, when, defines } of variants.slice()) {
+ variants.push({
+ id: id === "default" ? "node-island" : `${id}-node-island`,
+ when: { ...when, nodeIsland: true },
+ defines: [...defines, "SCR_NODE_ISLANDS"],
+ });
+ }
+ }
+ // The listener removal and signalCode entries of the same children.
+ if (source === "scr_child_lifecycle.c") {
+ variants.push({
+ id: "node-island",
+ when: { nodeIsland: true },
+ defines: ["SCR_NODE_ISLANDS"],
+ });
}
// A static executable cannot load NSS modules, so the os unit's passwd
// fallback (os.homedir() with HOME unset, and os.userInfo()) reads
diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c
index 096e573489f..ef119414695 100644
--- a/packages/runtime/src/scr_async.c
+++ b/packages/runtime/src/scr_async.c
@@ -1353,6 +1353,11 @@ static void scr_hosted_timer_push(ScrTimer t);
/* The hosted timer, immediate and tick operations (defined with the
* hosted loop below). Each answers false when the handle names nothing. */
enum { SCR_HOSTED_CLEAR, SCR_HOSTED_REF, SCR_HOSTED_UNREF, SCR_HOSTED_REFRESH, SCR_HOSTED_HAS_REF };
+/* A scr_loop_hosted_run kind beyond the SCR_NODE_JOB_* jobs: island code
+ * cleared one of the program's timers, or immediates, by its number. Bits 8
+ * and up hold the bits of the id's offset above the token's 32. */
+#define SCR_HOSTED_ISLAND_CLEAR 16u
+#define SCR_HOSTED_ISLAND_IMMEDIATE 8u
static bool scr_hosted_timer_op(unsigned long id, int op);
static double scr_hosted_set_immediate(ScrClosure *cb);
static bool scr_hosted_immediate_op(unsigned long id, int op);
@@ -1499,7 +1504,20 @@ void scr_set_timeout(ScrClosure *cb, double ms) {
* its callback runs, so the run loop re-pushes it only if the callback
* did not clear it. */
-static SCR_TL unsigned long scr_interval_next_id = 1;
+/* The first timer and immediate id is 1 above this base, and both kinds
+ * take their ids from that one sequence: a number names at most one handle,
+ * whichever side created it. A program with Node islands starts the
+ * sequence at 2^52, a range Node's own timer ids (async ids, counted from
+ * 1) do not reach, so island code that clears a number clears the program's
+ * handle or one of its own, never the other side's, and clearTimeout of an
+ * Immediate (or clearImmediate of a Timeout) finds nothing to clear, as in
+ * Node. */
+#if defined(SCR_NODE_ISLANDS) && __SIZEOF_LONG__ >= 8
+#define SCR_TIMER_ID_BASE (1UL << 52)
+#else
+#define SCR_TIMER_ID_BASE 0UL
+#endif
+static SCR_TL unsigned long scr_interval_next_id = SCR_TIMER_ID_BASE + 1;
static SCR_TL unsigned long scr_firing_id = 0; /* interval currently running */
/* The immediate whose callback is running (0 outside one). */
static SCR_TL unsigned long scr_immediate_firing = 0;
@@ -1840,8 +1858,9 @@ void scr_clear_interval(double handle) {
* FIFO order — and immediates queued while the phase runs wait for the
* NEXT turn (the phase snapshots its end index). The queue is an
* append-only array with a head cursor; clearImmediate NULLs the slot in
- * place (order and indices stay stable mid-phase). Ids ride their own
- * space — clearTimeout of an Immediate is a no-op, like Node. */
+ * place (order and indices stay stable mid-phase). An immediate's id comes
+ * from the sequence timers use, so its number never names a timeout:
+ * clearTimeout of an Immediate is a no-op, like Node. */
typedef struct {
unsigned long id;
ScrClosure *cb; /* owned; NULL once fired or cleared */
@@ -1850,7 +1869,6 @@ typedef struct {
static SCR_TL ScrImmediate *scr_immediates = NULL;
static SCR_TL size_t scr_nimmediates = 0, scr_immediates_head = 0, scr_immediates_cap = 0;
-static SCR_TL unsigned long scr_immediate_seq = 0;
/* Pending (queued, uncleared) immediates and the reffed subset — the
* loop's no-sleep signal and liveness count respectively. */
static SCR_TL size_t scr_pending_immediates = 0;
@@ -1865,7 +1883,7 @@ double scr_set_immediate(ScrClosure *cb) {
scr_immediates = realloc(scr_immediates, scr_immediates_cap * sizeof *scr_immediates);
if (!scr_immediates) scr_oom();
}
- unsigned long id = ++scr_immediate_seq;
+ unsigned long id = scr_interval_next_id++;
scr_immediates[scr_nimmediates].id = id;
scr_immediates[scr_nimmediates].cb = cb; /* ownership moves in */
scr_immediates[scr_nimmediates].reffed = true;
@@ -5811,7 +5829,7 @@ static bool scr_hosted_timer_op(unsigned long id, int op) {
}
static double scr_hosted_set_immediate(ScrClosure *cb) {
- unsigned long id = ++scr_immediate_seq;
+ unsigned long id = scr_interval_next_id++;
uint32_t token = scr_hjob_new(SCR_HJ_IMMEDIATE, cb);
scr_hjobs[token - 1].id = id;
scr_hjobs[token - 1].reffed = true;
@@ -5969,6 +5987,17 @@ void scr_loop_hosted_run(uint32_t kind, uint32_t token) {
scr_hosted_turn_ran = true;
return;
default:
+ /* Island code cleared one of the program's handles by its number
+ * (scr_node_island_host.cjs): the runtime's own clear of that handle,
+ * as if the program had called it, so its callback is released and the
+ * count of live timers stays right. Timers and immediates draw from one
+ * id sequence, so the other kind's clear finds nothing here, as in
+ * Node. */
+ if ((kind & SCR_HOSTED_ISLAND_CLEAR) != 0) {
+ double id = (double)((uint64_t)SCR_TIMER_ID_BASE + (((uint64_t)(kind >> 8)) << 32) + token);
+ if ((kind & SCR_HOSTED_ISLAND_IMMEDIATE) != 0) scr_clear_immediate(id);
+ else scr_clear_interval(id);
+ }
return;
}
scr_hosted_settle_exception();
diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h
index 8a5c7e7b26f..cd705be8a56 100644
--- a/packages/runtime/src/scr_async.h
+++ b/packages/runtime/src/scr_async.h
@@ -520,8 +520,9 @@ bool scr_timer_has_ref(double handle);
void scr_timer_refresh(double handle);
/* setImmediate/clearImmediate — Node's check phase: FIFO once per loop
* turn after due timers, immediates queued mid-phase wait for the next
- * turn. The handle is an f64 id in its own space (clearTimeout of an
- * Immediate is a no-op, like Node); the Immediate ref trio mirrors the
+ * turn. The handle is an f64 id from the timer sequence, so its number
+ * names no timeout (clearTimeout of an Immediate is a no-op, like Node);
+ * the Immediate ref trio mirrors the
* Timeout one (unref'd pending immediates don't keep the loop alive and
* never fire once nothing reffed remains). scr_timers_teardown sweeps
* the immediate queue too. */
diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c
index 309a2d6b9bd..bf3ce305598 100644
--- a/packages/runtime/src/scr_child.c
+++ b/packages/runtime/src/scr_child.c
@@ -27,6 +27,22 @@
#include "scr_child_flow.h"
#endif
+/* The Node island build (scr_child.h, "a child process a Node island
+ * made"): an entry point hands a handle whose `island` member is set to the
+ * island's table before it reads a native field. Every other build, and the
+ * Windows arm (islands are POSIX), compiles the guards away. */
+#if defined(SCR_NODE_ISLANDS) && !defined(_WIN32)
+#define SCR_CHILD_FOREIGN 1
+#define SCR_IF_ISLAND(handle, ...) \
+ do { \
+ if ((handle)->island != NULL) { __VA_ARGS__; } \
+ } while (0)
+#else
+#define SCR_IF_ISLAND(handle, ...) \
+ do { \
+ } while (0)
+#endif
+
typedef struct ScrIpc ScrIpc;
static ScrIpc *scr_ipc_new(ScrChildStream *reader, ScrChildWriter *writer);
static ScrIpc *scr_ipc_retain(ScrIpc *ipc);
@@ -4722,6 +4738,9 @@ struct ScrChildStream {
ScrChildFlow flow; /* pause()/resume() state (scr_child_flow.h) */
#endif
struct ScrChildStream *next; /* the service registry (+1) */
+#ifdef SCR_CHILD_FOREIGN
+ void *island; /* a Node island's stream: every entry point goes to scr_child_island_ops */
+#endif
};
struct ScrChildWriter {
@@ -4753,6 +4772,9 @@ struct ScrChildWriter {
ScrAlsCtx *als;
ScrAlsCtx *end_als;
struct ScrChildWriter *next; /* the service registry (+1) */
+#ifdef SCR_CHILD_FOREIGN
+ void *island; /* a Node island's stream: every entry point goes to scr_child_island_ops */
+#endif
};
static SCR_TL ScrChildStream *scr_child_streams = NULL;
@@ -4830,8 +4852,43 @@ struct ScrChild {
* a store): 'spawn', 'error', 'exit' and 'close' run in it. */
ScrAlsCtx *als;
struct ScrChild *next; /* the pending registry */
+#ifdef SCR_CHILD_FOREIGN
+ void *island; /* a Node island's child: every entry point goes to scr_child_island_ops */
+#endif
};
+#ifdef SCR_CHILD_FOREIGN
+/* The handles of a child a Node island made: no pipe, no pid, in no
+ * registry. scr_node_island_child.c makes them and sets the table. */
+const ScrChildIslandOps *scr_child_island_ops = NULL;
+
+ScrChild *scr_child_island_new(void *island) {
+ ScrChild *c = calloc(1, sizeof *c);
+ if (!c) scr_trap("scriptc: out of memory\n");
+ c->rc = 1;
+ c->island = island;
+ return c;
+}
+
+ScrChildStream *scr_child_stream_island_new(void *island) {
+ ScrChildStream *s = calloc(1, sizeof *s);
+ if (!s) scr_trap("scriptc: out of memory\n");
+ s->rc = 1;
+ s->fd = -1;
+ s->island = island;
+ return s;
+}
+
+ScrChildWriter *scr_child_writer_island_new(void *island) {
+ ScrChildWriter *w = calloc(1, sizeof *w);
+ if (!w) scr_trap("scriptc: out of memory\n");
+ w->rc = 1;
+ w->fd = -1;
+ w->island = island;
+ return w;
+}
+#endif
+
static SCR_TL ScrChild *scr_children = NULL; /* unsettled children (registry +1) */
static SCR_TL ScrChild *scr_children_closing = NULL; /* exited, waiting for stdio EOF */
static SCR_TL size_t scr_children_reffed_n = 0; /* unsettled AND reffed (liveness) */
@@ -5022,6 +5079,7 @@ ScrChild *scr_child_retain(ScrChild *c) {
void scr_child_release(ScrChild *c) {
if (!c || c->rc == SIZE_MAX) return;
if (--c->rc == 0) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->drop(c->island));
scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */
scr_str_release(c->err_msg);
scr_str_release(c->exec_display);
@@ -5085,6 +5143,7 @@ static void scr_child_stream_drop_listeners(ScrChildStream *s) {
void scr_child_stream_release(ScrChildStream *s) {
if (!s || s->rc == SIZE_MAX) return;
if (--s->rc == 0) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->drop(s->island));
scr_child_stream_drop_listeners(s); /* only reachable pre-'end' via leaks */
#ifdef SCR_CHILD_STREAM_FLOW
scr_child_flow_free(s);
@@ -5663,6 +5722,7 @@ static void scr_child_stream_arm(ScrChildStream *s) {
* and never fire, the stdin rule. */
void scr_child_stream_on_data(ScrChildStream *s, ScrClosure *cb /*moves*/,
ScrChildStreamDataFn fn, bool once) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->on(s->island, SCR_CHILD_EV_DATA, cb, (void *)fn, once); return);
#ifdef SCR_CHILD_STREAM_FLOW
if (scr_child_flow_on_data(s, cb, fn, NULL, once)) return;
#endif
@@ -5692,6 +5752,7 @@ void scr_child_stream_on_data(ScrChildStream *s, ScrClosure *cb /*moves*/,
void scr_child_stream_on_data_str(ScrChildStream *s, ScrClosure *cb /*moves*/,
ScrChildStreamDataStrFn fn, bool once) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->on(s->island, SCR_CHILD_EV_DATA_STR, cb, (void *)fn, once); return);
#ifdef SCR_CHILD_STREAM_FLOW
if (scr_child_flow_on_data(s, cb, NULL, fn, once)) return;
#endif
@@ -5716,6 +5777,7 @@ void scr_child_stream_on_data_str(ScrChildStream *s, ScrClosure *cb /*moves*/,
}
ScrChildStream *scr_child_stream_set_encoding(ScrChildStream *s, ScrStr *enc) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->set_encoding(s->island, enc); return scr_child_stream_retain(s));
scr_str_release(s->encoding);
s->encoding = scr_str_retain(enc);
s->dec_pending = 0;
@@ -5728,6 +5790,7 @@ ScrChildStream *scr_child_stream_set_encoding(ScrChildStream *s, ScrStr *enc) {
/* stream.on/once("end", cb): fires once at EOF; alone it neither reads
* nor keeps the loop alive (the stdin rule). */
void scr_child_stream_on_end(ScrChildStream *s, ScrClosure *cb /*moves*/, bool once) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->on(s->island, SCR_CHILD_EV_END, cb, NULL, once); return);
(void)once; /* 'end' fires at most once; the flag changes nothing */
#ifdef SCR_CHILD_STREAM_FLOW
if (scr_child_flow_on_end(s, cb, once)) return;
@@ -5749,9 +5812,11 @@ void scr_child_stream_on_end(ScrChildStream *s, ScrClosure *cb /*moves*/, bool o
/* child.stdout / child.stderr — +1 handle, or NULL when the slot was not
* piped (Node's null there). */
ScrChildStream *scr_child_stdout(ScrChild *c) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->stdio(c));
return c->out_stream ? scr_child_stream_retain(c->out_stream) : NULL;
}
ScrChildStream *scr_child_stderr(ScrChild *c) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->stdio(c));
return c->err_stream ? scr_child_stream_retain(c->err_stream) : NULL;
}
@@ -5802,6 +5867,7 @@ ScrChildWriter *scr_child_writer_retain(ScrChildWriter *w) {
void scr_child_writer_release(ScrChildWriter *w) {
if (!w || w->rc == SIZE_MAX) return;
if (--w->rc == 0) {
+ SCR_IF_ISLAND(w, scr_child_island_ops->drop(w->island));
scr_child_writer_dearm(w);
if (w->fd >= 0) close(w->fd);
free(w->buf);
@@ -6055,6 +6121,7 @@ static bool scr_child_writer_write(ScrChildWriter *w, const void *data,
}
bool scr_child_writer_write_string(ScrChildWriter *w, const ScrStr *data) {
+ SCR_IF_ISLAND(w, return scr_child_island_ops->write(w->island, data, NULL));
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
bool r = scr_child_writer_write(w, bytes, data->len);
@@ -6063,11 +6130,13 @@ bool scr_child_writer_write_string(ScrChildWriter *w, const ScrStr *data) {
}
bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) {
+ SCR_IF_ISLAND(w, return scr_child_island_ops->write(w->island, NULL, data));
SCR_BYTES_SNAPSHOT(data);
return scr_child_writer_write(w, data->data, data->len);
}
void scr_child_writer_end(ScrChildWriter *w) {
+ SCR_IF_ISLAND(w, scr_child_island_ops->call(w->island, "end"); return);
if (w->ending || w->finish_pending || w->finished || w->destroyed) return;
scr_als_ctx_release(w->end_als);
w->end_als = scr_als_capture();
@@ -6077,6 +6146,7 @@ void scr_child_writer_end(ScrChildWriter *w) {
}
void scr_child_writer_destroy(ScrChildWriter *w) {
+ SCR_IF_ISLAND(w, scr_child_island_ops->call(w->island, "destroy"); return);
if (w->finished || (w->destroyed && !w->error_pending)) return;
scr_child_writer_close(w);
scr_child_writer_clear_queue(w);
@@ -6091,7 +6161,10 @@ void scr_child_writer_destroy(ScrChildWriter *w) {
scr_child_writer_drop_listeners(w);
}
-bool scr_child_writer_writable(ScrChildWriter *w) { return w->writable; }
+bool scr_child_writer_writable(ScrChildWriter *w) {
+ SCR_IF_ISLAND(w, return scr_child_island_ops->boolean(w->island, SCR_CHILD_PROP_WRITABLE));
+ return w->writable;
+}
static void scr_child_writer_add_zero(ScrChildStreamEndL **ls, size_t *n,
size_t *cap, ScrClosure *cb, bool once) {
@@ -6106,6 +6179,7 @@ static void scr_child_writer_add_zero(ScrChildStreamEndL **ls, size_t *n,
void scr_child_writer_on_drain(ScrChildWriter *w, ScrClosure *cb /*moves*/,
bool once) {
+ SCR_IF_ISLAND(w, scr_child_island_ops->on(w->island, SCR_CHILD_EV_DRAIN, cb, NULL, once); return);
if (w->finished || (w->destroyed && !w->drain_pending)) {
scr_closure_release(cb);
return;
@@ -6115,6 +6189,7 @@ void scr_child_writer_on_drain(ScrChildWriter *w, ScrClosure *cb /*moves*/,
void scr_child_writer_on_finish(ScrChildWriter *w, ScrClosure *cb /*moves*/,
bool once) {
+ SCR_IF_ISLAND(w, scr_child_island_ops->on(w->island, SCR_CHILD_EV_FINISH, cb, NULL, once); return);
if (w->finished || (w->destroyed && !w->finish_pending)) {
scr_closure_release(cb);
return;
@@ -6124,6 +6199,8 @@ void scr_child_writer_on_finish(ScrChildWriter *w, ScrClosure *cb /*moves*/,
void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/,
ScrChildErrFn fn, bool once) {
+ SCR_IF_ISLAND(w, scr_child_island_ops->on(w->island, SCR_CHILD_EV_WRITE_ERROR, cb, (void *)fn, once);
+ return);
(void)once; /* a writer emits at most one terminal error */
if ((w->destroyed && !w->finished && !w->error_pending) ||
(w->err_msg != NULL && !w->error_pending)) {
@@ -6305,6 +6382,7 @@ static void scr_child_writer_child_settled(ScrChildWriter *w) {
}
ScrChildWriter *scr_child_stdin(ScrChild *c) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->stdio(c));
return c->in_stream ? scr_child_writer_retain(c->in_stream) : NULL;
}
@@ -6604,27 +6682,44 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
/* ── the lifecycle members (child.pid/exitCode/killed/kill/unref) ──────── */
/* child.pid — undefined exactly when the spawn failed (Node's shape). */
-bool scr_child_has_pid(ScrChild *c) { return c->state != SCR_CHILD_SPAWN_FAILED; }
-double scr_child_pid(ScrChild *c) { return (double)c->pid; }
+bool scr_child_has_pid(ScrChild *c) {
+ SCR_IF_ISLAND(c, double pid; return scr_child_island_ops->number(c->island, SCR_CHILD_PROP_PID, &pid));
+ return c->state != SCR_CHILD_SPAWN_FAILED;
+}
+double scr_child_pid(ScrChild *c) {
+ SCR_IF_ISLAND(c, double pid = 0; scr_child_island_ops->number(c->island, SCR_CHILD_PROP_PID, &pid);
+ return pid);
+ return (double)c->pid;
+}
/* child.exitCode — null while running and for a signal death; the exit
* code after a normal exit; -errno once a spawn failure SETTLED (Node
* flips it when the "error" event fires — before that it reads null). */
bool scr_child_has_exit_code(ScrChild *c) {
+ SCR_IF_ISLAND(c, double code;
+ return scr_child_island_ops->number(c->island, SCR_CHILD_PROP_EXIT_CODE, &code));
if (c->state == SCR_CHILD_EXITED) return c->has_code;
return c->state == SCR_CHILD_SPAWN_FAILED && c->settled;
}
double scr_child_exit_code(ScrChild *c) {
+ SCR_IF_ISLAND(c, double code = 0; scr_child_island_ops->number(c->island, SCR_CHILD_PROP_EXIT_CODE, &code);
+ return code);
if (c->state == SCR_CHILD_SPAWN_FAILED) return -(double)c->spawn_errno;
return c->code;
}
-bool scr_child_killed(ScrChild *c) { return c->killed; }
+bool scr_child_killed(ScrChild *c) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->boolean(c->island, SCR_CHILD_PROP_KILLED));
+ return c->killed;
+}
ScrStr *scr_child_spawnfile(ScrChild *c) {
+ SCR_IF_ISLAND(c, ScrStr *file = scr_child_island_ops->string(c->island, SCR_CHILD_PROP_SPAWNFILE);
+ return file ? file : scr_str_new("", 0));
return c->spawnfile ? scr_str_retain(c->spawnfile) : scr_str_new("", 0);
}
ScrArr *scr_child_spawnargs(ScrChild *c) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->spawnargs(c->island));
return c->spawnargs ? scr_arr_retain(c->spawnargs) : scr_arr_new(SCR_ELEM_STR, 0);
}
@@ -6647,6 +6742,7 @@ static bool scr_child_kill_signo(ScrChild *c, int signo) {
* (scr_signal_from_name; unknown names throw the ERR_UNKNOWN_SIGNAL
* TypeError BEFORE the handle check, like Node). */
bool scr_child_kill(ScrChild *c, const ScrStr *signal) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->kill(c->island, signal, 0));
int signo = scr_signal_from_name(signal);
if (signo < 0) {
size_t cap = 16 + signal->len + 1;
@@ -6662,6 +6758,7 @@ bool scr_child_kill(ScrChild *c, const ScrStr *signal) {
/* child.kill(number) — the numeric form passes through (0 probes). */
bool scr_child_kill_num(ScrChild *c, double signum) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->kill(c->island, NULL, signum));
return scr_child_kill_signo(c, (int)signum);
}
@@ -6671,6 +6768,7 @@ bool scr_child_kill_num(ScrChild *c, double signum) {
* never reaps is left to the OS at process exit (init reparents it — no
* zombie outlives the parent). */
void scr_child_unref(ScrChild *c) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->call(c->island, "unref"); return);
if (c->reffed) {
c->reffed = false;
scr_children_reffed_n--; /* settle/teardown clear reffed first */
@@ -6762,6 +6860,7 @@ void scr_children_teardown(void) {
}
void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->on(c->island, SCR_CHILD_EV_EXIT, cb, (void *)fn, false); return);
if (c->settled) {
scr_closure_release(cb); /* after the terminal event: never fires */
return;
@@ -6777,6 +6876,7 @@ void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn)
}
void scr_child_on_close(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->on(c->island, SCR_CHILD_EV_CLOSE, cb, (void *)fn, false); return);
if (c->close_done) {
scr_closure_release(cb);
return;
@@ -6825,6 +6925,7 @@ static ScrError *scr_child_spawn_error(ScrChild *c) {
void scr_child_error_event_install(void) { scr_child_spawn_error_hook = &scr_child_spawn_error; }
void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->on(c->island, SCR_CHILD_EV_ERROR, cb, (void *)fn, false); return);
if (c->settled) {
scr_closure_release(cb);
return;
@@ -7663,6 +7764,7 @@ static void scr_child_drop_spawn_listeners(ScrChild *c) {
}
void scr_child_ref(ScrChild *c) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->call(c->island, "ref"); return);
if (!c->settled && !c->reffed) {
c->reffed = true;
scr_children_reffed_n++;
@@ -7670,6 +7772,7 @@ void scr_child_ref(ScrChild *c) {
}
void scr_child_on_spawn(ScrChild *c, ScrClosure *cb) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->on(c->island, SCR_CHILD_EV_SPAWN, cb, NULL, false); return);
if (c->spawn_done || c->settled) { scr_closure_release(cb); return; }
if (c->n_spawn == c->cap_spawn) {
c->cap_spawn = c->cap_spawn ? c->cap_spawn * 2 : 2;
@@ -8576,24 +8679,35 @@ double scr_process_fork_target(double target_count) {
}
/* The child was created with an IPC channel; it keeps it until it is freed. */
-bool scr_child_ipc_present(ScrChild *c) { return c->ipc != NULL; }
+bool scr_child_ipc_present(ScrChild *c) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->boolean(c->island, SCR_CHILD_PROP_CHANNEL));
+ return c->ipc != NULL;
+}
bool scr_child_ipc_connected(ScrChild *c) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->boolean(c->island, SCR_CHILD_PROP_CONNECTED));
return c->ipc != NULL && c->ipc->connected;
}
bool scr_child_ipc_send(ScrChild *c, ScrStr *json) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->send(c->island, json, NULL, NULL));
return scr_ipc_send(c->ipc, json, NULL, NULL);
}
bool scr_child_ipc_send_cb(ScrChild *c, ScrStr *json, ScrClosure *cb,
ScrIpcSendFn fn) {
+ SCR_IF_ISLAND(c, return scr_child_island_ops->send(c->island, json, cb, fn));
return scr_ipc_send(c->ipc, json, cb, fn);
}
-void scr_child_ipc_disconnect(ScrChild *c) { scr_ipc_disconnect(c->ipc); }
+void scr_child_ipc_disconnect(ScrChild *c) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->call(c->island, "disconnect"); return);
+ scr_ipc_disconnect(c->ipc);
+}
void scr_child_ipc_on_message(ScrChild *c, ScrClosure *cb,
ScrIpcMessageFn fn, bool once) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->on(c->island, SCR_CHILD_EV_MESSAGE, cb, (void *)fn, once); return);
scr_ipc_add_message(c->ipc, cb, fn, once);
}
void scr_child_ipc_on_disconnect(ScrChild *c, ScrClosure *cb, bool once) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->on(c->island, SCR_CHILD_EV_DISCONNECT, cb, NULL, once); return);
scr_ipc_add_disconnect(c->ipc, cb, once);
}
@@ -8753,6 +8867,9 @@ static void scr_child_bridge_write_taken(ScrClosure *cb, ScrStr *message) {
static void scr_child_bridge_write(ScrClosure *cb, ScrStream *stream, ScrBytes *chunk) {
ScrChildWriter *writer = scr_box_get_ref(cb->caps[0]);
+ SCR_IF_ISLAND(writer,
+ scr_child_island_ops->bridge_write(writer->island, stream, chunk, scr_child_stream_ops);
+ scr_child_writer_release(writer); return);
scr_child_writer_write_bytes(writer, chunk);
bool pending = scr_child_writer_queued(writer) > 0;
#ifdef _WIN32
@@ -8780,6 +8897,8 @@ static void scr_child_bridge_write(ScrClosure *cb, ScrStream *stream, ScrBytes *
}
static void scr_child_bridge_final(ScrClosure *cb, ScrStream *stream) {
ScrChildWriter *writer = scr_box_get_ref(cb->caps[0]);
+ SCR_IF_ISLAND(writer, scr_child_island_ops->bridge_final(writer->island, stream, scr_child_stream_ops);
+ scr_child_writer_release(writer); return);
scr_child_writer_on_error(writer,
scr_child_bridge_operation(stream, writer, (void *)scr_child_bridge_final_error),
scr_child_bridge_final_error, true);
@@ -8943,6 +9062,7 @@ static ScrDyn *scr_child_dyn_invoke(
static ScrDyn *scr_child_dyn_get(void *h, const char *key, size_t key_len) {
ScrChild *child = (ScrChild *)h;
+ SCR_IF_ISLAND(child, scr_child_island_ops->stdio(child));
if (key_len == 6 && !memcmp(key, "stdout", 6)) return scr_child_bridge_readable(child->out_stream, &child->out_dyn);
if (key_len == 6 && !memcmp(key, "stderr", 6)) return scr_child_bridge_readable(child->err_stream, &child->err_dyn);
if (key_len == 5 && !memcmp(key, "stdin", 5)) return scr_child_bridge_writable(child->in_stream, &child->in_dyn);
@@ -9006,6 +9126,7 @@ static void scr_child_loop_install(void) {
}
void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener) {
+ SCR_IF_ISLAND(child, scr_child_island_ops->on_dyn(child->island, event, listener); return);
scr_child_dyn_install();
ScrDyn *self = scr_dyn_new_handle(child, SCR_DYNH_CHILD);
ScrDyn *name = scr_dyn_new_str(event);
diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h
index 9524a35e4ce..28ea0343006 100644
--- a/packages/runtime/src/scr_child.h
+++ b/packages/runtime/src/scr_child.h
@@ -369,6 +369,74 @@ extern const ScrChildStreamOps scr_stream_child_ops;
ScrDyn *scr_stream_dyn_view(ScrStream *stream); /* borrowed; +1 live method/property view */
void scr_child_dyn_streams_install(const ScrChildStreamOps *ops,
ScrDyn *(*readable_box)(void *), ScrDyn *(*writable_box)(void *));
+/* ── a child process a Node island made (the SCR_NODE_ISLANDS build) ─────
+ * An island's ChildProcess that enters a native ChildProcess slot is held
+ * as a ScrChild with no state of its own: its `island` member names the
+ * island's object, and every entry point that takes a child, one of its
+ * output streams or its input writer hands such a value to the table
+ * below, which calls the island object's own member (scr_node_island_child.c,
+ * linked only into programs with both Node islands and child_process). The
+ * guards exist only in the SCR_NODE_ISLANDS build of scr_child.c and
+ * scr_child_lifecycle.c, which programs with Node islands select; every
+ * other program keeps the default objects. `island` is the same kind of
+ * pointer for all three handles. */
+enum {
+ SCR_CHILD_EV_SPAWN = 0,
+ SCR_CHILD_EV_EXIT,
+ SCR_CHILD_EV_CLOSE,
+ SCR_CHILD_EV_ERROR,
+ SCR_CHILD_EV_MESSAGE,
+ SCR_CHILD_EV_DISCONNECT,
+ SCR_CHILD_EV_DATA, /* a stream's 'data', chunks as Buffers */
+ SCR_CHILD_EV_DATA_STR, /* a stream's 'data', chunks as strings */
+ SCR_CHILD_EV_END,
+ SCR_CHILD_EV_DRAIN,
+ SCR_CHILD_EV_FINISH,
+ SCR_CHILD_EV_WRITE_ERROR, /* the writer's 'error' */
+};
+enum {
+ SCR_CHILD_PROP_PID = 0,
+ SCR_CHILD_PROP_EXIT_CODE,
+ SCR_CHILD_PROP_SIGNAL_CODE,
+ SCR_CHILD_PROP_KILLED,
+ SCR_CHILD_PROP_CONNECTED,
+ SCR_CHILD_PROP_CHANNEL, /* created with an IPC channel */
+ SCR_CHILD_PROP_SPAWNFILE,
+ SCR_CHILD_PROP_WRITABLE, /* the writer's */
+ SCR_CHILD_PROP_PAUSED, /* a readable's isPaused() */
+ SCR_CHILD_PROP_FLOWING, /* a readable's readableFlowing: a number, 1 or 0; null is "no value" */
+};
+typedef struct ScrChildIslandOps {
+ /* The handle that names `island` is being freed. */
+ void (*drop)(void *island);
+ /* Fills the child's three stdio handles from the island's (once). */
+ void (*stdio)(ScrChild *child);
+ /* emitter.on / once(event, listener): `fn` is the entry point's adapter
+ * for `event` (NULL where the listener takes no argument). Moves cb. */
+ void (*on)(void *island, int event, ScrClosure *cb, void *fn, bool once);
+ void (*off)(void *island, int event, ScrClosure *cb); /* borrows cb */
+ void (*on_dyn)(void *island, ScrStr *event, ScrDyn *listener);
+ bool (*number)(void *island, int prop, double *value); /* false: null or undefined */
+ bool (*boolean)(void *island, int prop);
+ ScrStr *(*string)(void *island, int prop); /* +1; NULL: null or undefined */
+ ScrArr *(*spawnargs)(void *island); /* +1 */
+ bool (*kill)(void *island, const ScrStr *signal, double signum); /* signal NULL: the number */
+ void (*call)(void *island, const char *method); /* a method without arguments or result */
+ bool (*send)(void *island, ScrStr *json, ScrClosure *cb, ScrIpcSendFn fn); /* moves cb */
+ void (*set_encoding)(void *island, ScrStr *encoding);
+ bool (*write)(void *island, const ScrStr *text, const ScrBytes *bytes); /* one of the two */
+ /* The general stream representation's writes into the writer: the
+ * stream's write or final completes when the island's callback runs. */
+ void (*bridge_write)(void *island, ScrStream *stream, ScrBytes *chunk,
+ const ScrChildStreamOps *stream_ops);
+ void (*bridge_final)(void *island, ScrStream *stream, const ScrChildStreamOps *stream_ops);
+} ScrChildIslandOps;
+/* Defined by the SCR_NODE_ISLANDS build of scr_child.c; set with the first
+ * island child a program adopts. */
+extern const ScrChildIslandOps *scr_child_island_ops;
+ScrChild *scr_child_island_new(void *island); /* +1 */
+ScrChildStream *scr_child_stream_island_new(void *island); /* +1 */
+ScrChildWriter *scr_child_writer_island_new(void *island); /* +1 */
bool scr_children_reffed_pending(void);
void scr_children_teardown(void);
/* Node's signal-name table (scr_lib.c), shared with child.kill: the
diff --git a/packages/runtime/src/scr_child_flow_impl.h b/packages/runtime/src/scr_child_flow_impl.h
index a3ed8bd2d69..e538f28e8e9 100644
--- a/packages/runtime/src/scr_child_flow_impl.h
+++ b/packages/runtime/src/scr_child_flow_impl.h
@@ -376,25 +376,32 @@ static void scr_child_flow_resume(ScrChildStream *s) {
/* ── the members ─────────────────────────────────────────────────────── */
-/* stream.pause(): +1 receiver (Node returns the stream). */
+/* stream.pause(): +1 receiver (Node returns the stream). A stream of a child
+ * a Node island made is the island's: its own pause, resume, isPaused and
+ * readableFlowing answer, as for every other member. */
ScrChildStream *scr_child_stream_pause(ScrChildStream *s) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->call(s->island, "pause"); return scr_child_stream_retain(s));
if (s->flow.state != SCR_CHILD_FLOW_PAUSED) scr_child_flow_enter(s, SCR_CHILD_FLOW_PAUSED);
return scr_child_stream_retain(s);
}
/* stream.resume(): +1 receiver. */
ScrChildStream *scr_child_stream_resume(ScrChildStream *s) {
+ SCR_IF_ISLAND(s, scr_child_island_ops->call(s->island, "resume"); return scr_child_stream_retain(s));
scr_child_flow_resume(s);
return scr_child_stream_retain(s);
}
bool scr_child_stream_is_paused(ScrChildStream *s) {
+ SCR_IF_ISLAND(s, return scr_child_island_ops->boolean(s->island, SCR_CHILD_PROP_PAUSED));
return s->flow.state == SCR_CHILD_FLOW_PAUSED;
}
/* stream.readableFlowing: 0 false, 1 true, -1 null (no 'data' listener was
* ever registered and the stream was never paused or resumed). */
double scr_child_stream_flowing(ScrChildStream *s) {
+ SCR_IF_ISLAND(s, double flowing = 0;
+ return scr_child_island_ops->number(s->island, SCR_CHILD_PROP_FLOWING, &flowing) ? flowing : -1);
if (s->flow.state == SCR_CHILD_FLOW_PAUSED) return 0;
if (s->flow.state == SCR_CHILD_FLOW_FLOWING || s->flow.listened) return 1;
return -1;
diff --git a/packages/runtime/src/scr_child_lifecycle.c b/packages/runtime/src/scr_child_lifecycle.c
index c374d777bab..84e0a1bb733 100644
--- a/packages/runtime/src/scr_child_lifecycle.c
+++ b/packages/runtime/src/scr_child_lifecycle.c
@@ -17,8 +17,14 @@
* null for a normal exit, while the child runs, and after a spawn
* failure). The name is the runtime's static signal spelling retained
* into a fresh +1 ScrStr. */
-bool scr_child_has_signal_code(ScrChild *c) { return c->exit_signal != NULL; }
+bool scr_child_has_signal_code(ScrChild *c) {
+ SCR_IF_ISLAND(c, ScrStr *name = scr_child_island_ops->string(c->island, SCR_CHILD_PROP_SIGNAL_CODE);
+ bool has = name != NULL; scr_str_release(name); return has);
+ return c->exit_signal != NULL;
+}
ScrStr *scr_child_signal_code(ScrChild *c) {
+ SCR_IF_ISLAND(c, ScrStr *name = scr_child_island_ops->string(c->island, SCR_CHILD_PROP_SIGNAL_CODE);
+ return name ? name : scr_str_new("", 0));
return scr_str_new(c->exit_signal, strlen(c->exit_signal));
}
@@ -47,6 +53,7 @@ ScrStr *scr_child_signal_code(ScrChild *c) {
* and Node-exact. The 'spawn' emit detaches its array before invoking its
* listeners, so removal during it already finds an empty registry. */
void scr_child_off_spawn(ScrChild *c, ScrClosure *cb) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->off(c->island, SCR_CHILD_EV_SPAWN, cb); return);
for (size_t i = c->n_spawn; i > 0; i--) {
if (!scr_closure_identity_equal(c->spawn_cbs[i - 1], cb)) continue;
scr_closure_release(c->spawn_cbs[i - 1]);
@@ -57,6 +64,7 @@ void scr_child_off_spawn(ScrChild *c, ScrClosure *cb) {
}
void scr_child_off_exit(ScrChild *c, ScrClosure *cb) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->off(c->island, SCR_CHILD_EV_EXIT, cb); return);
if (c->settled) return; /* the 'exit' emit is in flight (or done) */
for (size_t i = c->n_exit; i > 0; i--) {
if (!scr_closure_identity_equal(c->exit_cbs[i - 1].cb, cb)) continue;
@@ -68,6 +76,7 @@ void scr_child_off_exit(ScrChild *c, ScrClosure *cb) {
}
void scr_child_off_close(ScrChild *c, ScrClosure *cb) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->off(c->island, SCR_CHILD_EV_CLOSE, cb); return);
if (c->close_done) return; /* the 'close' emit is in flight (or done) */
for (size_t i = c->n_close; i > 0; i--) {
if (!scr_closure_identity_equal(c->close_cbs[i - 1].cb, cb)) continue;
@@ -79,6 +88,7 @@ void scr_child_off_close(ScrChild *c, ScrClosure *cb) {
}
void scr_child_off_error(ScrChild *c, ScrClosure *cb) {
+ SCR_IF_ISLAND(c, scr_child_island_ops->off(c->island, SCR_CHILD_EV_ERROR, cb); return);
if (c->settled) return; /* the 'error' emit is in flight (or done) */
for (size_t i = c->n_err; i > 0; i--) {
if (!scr_closure_identity_equal(c->err_cbs[i - 1].cb, cb)) continue;
diff --git a/packages/runtime/src/scr_child_private.h b/packages/runtime/src/scr_child_private.h
index b5c6fd5cba4..5556c9943a6 100644
--- a/packages/runtime/src/scr_child_private.h
+++ b/packages/runtime/src/scr_child_private.h
@@ -143,8 +143,23 @@ struct ScrChild {
* a store): 'spawn', 'error', 'exit' and 'close' run in it. */
ScrAlsCtx *als;
struct ScrChild *next; /* the pending registry */
+#ifdef SCR_NODE_ISLANDS
+ void *island; /* a Node island's child: every entry point goes to scr_child_island_ops */
+#endif
};
#endif /* _WIN32 */
+/* The Node island build's guard (scr_child.c defines the same). */
+#if defined(SCR_NODE_ISLANDS) && !defined(_WIN32)
+#define SCR_IF_ISLAND(handle, ...) \
+ do { \
+ if ((handle)->island != NULL) { __VA_ARGS__; } \
+ } while (0)
+#else
+#define SCR_IF_ISLAND(handle, ...) \
+ do { \
+ } while (0)
+#endif
+
#endif /* SCR_CHILD_PRIVATE_H */
diff --git a/packages/runtime/src/scr_node_island.h b/packages/runtime/src/scr_node_island.h
index f82cef2eede..ae29089d1c1 100644
--- a/packages/runtime/src/scr_node_island.h
+++ b/packages/runtime/src/scr_node_island.h
@@ -203,6 +203,9 @@ int scr_ni_exit_code(bool set, double code, double *out);
void scr_node_island_set_exit_code(double code);
double scr_node_island_exit_code(void);
bool scr_node_island_has_exit_code(void);
+/* The libCall of process.chdir in a program with islands: Node's cached
+ * process.cwd() follows the program's change. */
+void scr_node_island_chdir(ScrStr *dir);
/* A native error is one V8 error while native code holds it. */
void scr_ni_error_link(ScrDyn *value);
ScrDyn *scr_ni_helper(const char *name, ScrDyn *const *args, size_t argc);
@@ -510,6 +513,10 @@ bool scr_node_island_instanceof(ScrDyn *value, ScrDyn *ctor);
bool scr_node_island_instanceof_global(ScrDyn *value, ScrStr *name);
ScrDyn *scr_node_island_invoke(ScrDyn *obj, ScrStr *key, ScrDyn *args);
ScrDyn *scr_node_island_promisify(ScrDyn *fn);
+/* clearTimeout / clearInterval / clearImmediate (which 0, 1, 2) of a
+ * checked-dynamic value that is not a number: an island timer is cleared in
+ * Node, any other value is ignored. */
+void scr_node_island_clear_timer(ScrDyn *value, double which);
void scr_node_island_set(ScrDyn *obj, ScrStr *key, ScrDyn *value);
void scr_node_island_set_key(ScrDyn *obj, ScrDyn *key, ScrDyn *value);
ScrDyn *scr_node_island_subclass(ScrDyn *base, ScrStr *name, ScrDyn *ctor, ScrDyn *fields, ScrDyn *methods, ScrDyn *getters, ScrDyn *setters);
diff --git a/packages/runtime/src/scr_node_island_child.c b/packages/runtime/src/scr_node_island_child.c
new file mode 100644
index 00000000000..5dea1521d3d
--- /dev/null
+++ b/packages/runtime/src/scr_node_island_child.c
@@ -0,0 +1,811 @@
+/* Node islands: a child process an island made, in a native ChildProcess
+ * slot. Linked only into programs with both Node islands and child_process
+ * (the `islandChild` link feature), with the SCR_NODE_ISLANDS build of
+ * scr_child.c.
+ *
+ * A ChildProcess slot holds a ScrChild that compiled code calls scr_child_*
+ * on, so an island's ChildProcess cannot sit in it as it is. It is adopted
+ * where it enters the slot, the checked conversion (scr_ni_child_unbox): the
+ * slot gets a ScrChild that has no state of its own and names the island's
+ * object. Every entry point that takes it hands it to the table at the end
+ * of this unit (scr_child.h, ScrChildIslandOps), which calls the island
+ * object's own member:
+ *
+ * - a listener is registered on the island's emitter, as a function of
+ * this unit that calls the program's listener through the adapter the
+ * entry point was given. So the order of 'spawn', 'data', 'exit',
+ * 'close' and 'error', also relative to listeners the island's own code
+ * registered, is whatever Node's ChildProcess emits, and `once` and
+ * removal are the emitter's;
+ * - a property is read from the island's object at that moment, so
+ * nothing is copied that could go stale;
+ * - a method is the island's method.
+ *
+ * Converting the handle back to a dynamic value answers the island's
+ * object (scr_ni_child_box), so untyped code and code in the island use the
+ * object itself.
+ *
+ * Identity: one ScrChild per island object while the program holds it (the
+ * table below, keyed by the import). Lifetime: the ScrChild holds the
+ * island's object. While the process runs or a listener is attached the
+ * island's own emitter holds the listener functions, so events arrive
+ * whether or not the program still holds the ScrChild.
+ *
+ * Cycles: a listener that captures its own typed child is island child ->
+ * listener function -> the program's closure -> ScrChild -> island child. A
+ * ScrChild has no trace header (scr_child.c: "children allocate lean"), so
+ * the cross-heap collector cannot see that edge. Native children break the
+ * same cycle by releasing their listeners at the terminal event; here the
+ * island child's 'close' does it: one function per island child (the hook)
+ * listens for it, and the program's closures behind every listener
+ * registered through a ScrChild of that island child, or through its stdio
+ * handles, are released when it has been delivered. The listener functions
+ * themselves stay on the island's emitter, as Node keeps them. */
+#define _XOPEN_SOURCE 700
+#define _DARWIN_C_SOURCE 1
+/* This unit is linked only into a program with islands and always reads the
+ * ScrChild layout of scr_child.c's SCR_NODE_ISLANDS build: it has no block
+ * that depends on the define. */
+#undef SCR_NODE_ISLANDS
+#define SCR_NODE_ISLANDS 1
+#include "scr_runtime.h"
+#include "scr_child_private.h"
+#include "scr_node_island.h"
+
+#include
+#include
+#include
+#include
+
+#if !defined(_WIN32) && !defined(__wasi__)
+
+SCR_COLD static void nc_oom(void) { scr_trap("scriptc: out of memory\n"); }
+
+/* ── the island's side ────────────────────────────────────────────────
+ * What needs the island's own globals, evaluated in the island when the
+ * first island object reaches a ChildProcess slot (nothing at start-up).
+ * `hooks` maps an island child to its hook function, so every ScrChild
+ * that is ever made for it finds the same one. */
+static const char NC_ISLAND_SOURCE[] =
+ "(function () {\n"
+ " const { ChildProcess } = process.getBuiltinModule('node:child_process');\n"
+ " const hooks = new WeakMap();\n"
+ " return {\n"
+ " isChild(value) { return value instanceof ChildProcess; },\n"
+ " hook(child) { return hooks.get(child); },\n"
+ /* True: the child's 'close' was already emitted, so the hook will not run. */
+ " setHook(child, hook) {\n"
+ " hooks.set(child, hook);\n"
+ " if (child._closesNeeded !== undefined && child._closesGot >= child._closesNeeded) return true;\n"
+ " child.once('close', hook);\n"
+ " return false;\n"
+ " },\n"
+ /* True: this call removed a registration of `listener`. */
+ " off(emitter, event, listener) {\n"
+ " const before = emitter.listenerCount(event);\n"
+ " emitter.removeListener(event, listener);\n"
+ " return emitter.listenerCount(event) < before;\n"
+ " },\n"
+ " channel(child) { return typeof child.send === 'function'; },\n"
+ " send(child, json, callback) {\n"
+ " const message = JSON.parse(json);\n"
+ " return callback === undefined ? child.send(message) : child.send(message, callback);\n"
+ " },\n"
+ /* The channel is JSON in the native runtime: a message arrives as its text. */
+ " onMessage(child, listener, once) {\n"
+ " const forward = (message) => listener(JSON.stringify(message));\n"
+ " if (once) child.once('message', forward); else child.on('message', forward);\n"
+ " },\n"
+ " spawnargs(child) { return JSON.stringify(child.spawnargs); },\n"
+ " };\n"
+ "})()";
+
+static SCR_TL ScrDyn *nc_island = NULL; /* the object the source above answers */
+
+static void nc_cleanup(void) {
+ scr_dyn_release(nc_island);
+ nc_island = NULL;
+}
+
+static bool nc_island_load(void) {
+ if (nc_island != NULL) return true;
+ ScrStr *specifier = scr_str_new("node:vm", 7);
+ ScrDyn *vm = scr_ni_require(specifier);
+ scr_str_release(specifier);
+ if (vm == NULL) return false;
+ ScrStr *text = scr_str_new(NC_ISLAND_SOURCE, sizeof NC_ISLAND_SOURCE - 1);
+ ScrDyn *source = scr_dyn_new_str(text);
+ scr_str_release(text);
+ nc_island = scr_ni_call_method(vm, "runInThisContext", 16, &source, 1);
+ scr_dyn_release(source);
+ scr_dyn_release(vm);
+ if (nc_island == NULL) return false;
+ scr_atexit(nc_cleanup);
+ return true;
+}
+
+/* A function of the island's side (+1, or NULL with the exception pending). */
+static ScrDyn *nc_island_call(const char *name, ScrDyn *const *args, size_t argc) {
+ if (!nc_island_load()) return NULL;
+ return scr_ni_call_method(nc_island, name, strlen(name), args, argc);
+}
+
+static bool nc_true(ScrDyn *answer) {
+ bool yes = answer != NULL && answer->kind == SCR_DYN_BOOL && answer->v.b;
+ scr_dyn_release(answer);
+ return yes;
+}
+
+/* ── listeners ────────────────────────────────────────────────────────
+ * The function this unit registers on an island emitter is a closure with
+ * two captures: its NcListener (what to call, and how) and the program's
+ * closure. The second is an ordinary function capture, so the collectors
+ * trace what the program's closure holds. */
+enum {
+ NC_HOOK = 64, /* the island child's 'close' was delivered */
+ NC_DYN, /* an untyped listener: called with the arguments as they are */
+ NC_SEND, /* send()'s callback */
+ NC_BRIDGE_WRITE, /* a general stream's write into the island's stdin completed */
+ NC_BRIDGE_FINAL, /* its end completed */
+};
+
+typedef struct NcGroup {
+ size_t rc;
+ struct NcListener *first; /* newest first; each belongs to its function */
+ bool closed; /* the island child's 'close' was delivered */
+} NcGroup;
+
+typedef struct NcListener {
+ size_t rc;
+ NcGroup *group; /* +1; NULL for a callback that is no listener of the child */
+ struct NcListener *prev, *next;
+ ScrClosure *wrapper; /* the function this belongs to (not owned) */
+ void *fn; /* the entry point's adapter, NULL when the listener takes nothing */
+ ScrDyn *dyn; /* NC_DYN: the untyped listener (+1 until released) */
+ ScrStream *stream; /* NC_BRIDGE_*: the stream to complete (+1 through stream_ops) */
+ const ScrChildStreamOps *stream_ops;
+ uint8_t event; /* SCR_CHILD_EV_* or NC_* */
+ bool removed; /* off() took it from the emitter: no later off() finds it */
+ bool on_close; /* NC_DYN: registered for 'close', so it runs in the emit that closes the group */
+} NcListener;
+
+static NcGroup *nc_group_retain(NcGroup *g) {
+ g->rc++;
+ return g;
+}
+
+static void nc_group_release(NcGroup *g) {
+ if (g != NULL && --g->rc == 0) free(g);
+}
+
+static void *nc_listener_retain(void *p) {
+ ((NcListener *)p)->rc++;
+ return p;
+}
+
+static void nc_listener_release(void *p) {
+ NcListener *l = p;
+ if (l == NULL || --l->rc != 0) return;
+ if (l->group != NULL) {
+ if (l->prev != NULL) l->prev->next = l->next;
+ else l->group->first = l->next;
+ if (l->next != NULL) l->next->prev = l->prev;
+ nc_group_release(l->group);
+ }
+ scr_dyn_release(l->dyn);
+ if (l->stream != NULL) l->stream_ops->release(l->stream);
+ free(l);
+}
+
+static NcListener *nc_listener_of(ScrClosure *wrapper) { /* +1 */
+ return scr_box_get_ref(wrapper->caps[0]);
+}
+
+/* The program's closure behind a function (+1), NULL once it was released
+ * or when the function never had one. */
+static ScrClosure *nc_closure_of(ScrClosure *wrapper) {
+ return wrapper->caps[1]->slot != 0 ? scr_box_get_ref(wrapper->caps[1]) : NULL;
+}
+
+static ScrDyn *nc_listener_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc);
+
+/* The function for one registration (+1). Moves cb (may be NULL). */
+static ScrDyn *nc_listener_new(NcGroup *group, int event, ScrClosure *cb, void *fn, NcListener **out) {
+ NcListener *l = calloc(1, sizeof *l);
+ if (l == NULL) nc_oom();
+ l->rc = 1;
+ l->event = (uint8_t)event;
+ l->fn = fn;
+ if (group != NULL) {
+ l->group = nc_group_retain(group);
+ l->next = group->first;
+ if (group->first != NULL) group->first->prev = l;
+ group->first = l;
+ }
+ ScrClosure *wrapper = scr_closure_new((void *)nc_listener_thunk, 2);
+ wrapper->caps[0] = scr_box_new_obj(nc_listener_retain, nc_listener_release, NULL);
+ scr_box_set_ref(wrapper->caps[0], l);
+ wrapper->caps[1] = scr_box_new(SCR_BOX_FUNC);
+ if (cb != NULL) scr_box_set_ref(wrapper->caps[1], cb);
+ l->wrapper = wrapper;
+ if (out != NULL) *out = l;
+ return scr_dyn_new_func(wrapper, nc_listener_thunk, 0, "(...args)", "");
+}
+
+/* The same function again as a value (+1): one closure, so one function in
+ * the island. */
+static ScrDyn *nc_listener_value(NcListener *l) {
+ return scr_dyn_new_func(scr_closure_retain(l->wrapper), nc_listener_thunk, 0, "(...args)", "");
+}
+
+/* The 'close' of an island child: the program's closures behind every
+ * listener of its group are released, except those of 'close' itself, typed
+ * or untyped, which are still to run in this emit and release themselves
+ * when they have. */
+static void nc_group_close(NcGroup *group) {
+ group->closed = true;
+ size_t n = 0;
+ for (NcListener *l = group->first; l != NULL; l = l->next) n++;
+ if (n == 0) return;
+ /* Nothing is released while the list is walked: a release can free a
+ * handle, which enters the island, which can free a listener. */
+ ScrClosure **closures = calloc(n, sizeof *closures);
+ ScrDyn **dyns = calloc(n, sizeof *dyns);
+ if (closures == NULL || dyns == NULL) nc_oom();
+ size_t i = 0;
+ for (NcListener *l = group->first; l != NULL; l = l->next, i++) {
+ if (l->event == SCR_CHILD_EV_CLOSE || l->event == NC_HOOK || l->on_close) continue;
+ closures[i] = nc_closure_of(l->wrapper);
+ if (closures[i] != NULL) scr_box_set_ref(l->wrapper->caps[1], NULL);
+ dyns[i] = l->dyn;
+ l->dyn = NULL;
+ }
+ for (i = 0; i < n; i++) {
+ scr_closure_release(closures[i]);
+ scr_dyn_release(dyns[i]);
+ }
+ free(closures);
+ free(dyns);
+}
+
+/* A value the island delivered that the listener's type cannot take: a
+ * TypeError where the listener would have run. */
+SCR_COLD static void nc_refuse(const char *what) {
+ char text[240];
+ int len = snprintf(text, sizeof text, "scriptc: %s (a child process made in a Node island)", what);
+ scr_throw_error_msg(SCR_ERR_TYPE, text, (size_t)len);
+}
+
+/* The signal name an 'exit' or 'close' carries, as the static spelling the
+ * adapters take; a name outside the runtime's table is kept for the
+ * process. */
+static const char *nc_signal_name(const ScrStr *name) {
+ int number = scr_signal_from_name(name);
+ const char *known = number >= 0 ? scr_signal_name(number) : NULL;
+ if (known != NULL && strlen(known) == name->len && memcmp(known, name->data, name->len) == 0)
+ return known;
+ static SCR_TL char **kept = NULL;
+ static SCR_TL size_t nkept = 0;
+ for (size_t i = 0; i < nkept; i++)
+ if (strlen(kept[i]) == name->len && memcmp(kept[i], name->data, name->len) == 0) return kept[i];
+ char **grown = realloc(kept, (nkept + 1) * sizeof *grown);
+ char *copy = malloc(name->len + 1);
+ if (grown == NULL || copy == NULL) nc_oom();
+ memcpy(copy, name->data, name->len);
+ copy[name->len] = '\0';
+ kept = grown;
+ kept[nkept++] = copy;
+ return copy;
+}
+
+/* The Error an island handed over (+1), or NULL with a TypeError pending
+ * when the value is not one. */
+static ScrError *nc_error_of(ScrDyn *value, const char *what) {
+ if (value != NULL && value->kind == SCR_DYN_OBJ) return scr_error_from_dyn(value);
+ nc_refuse(what);
+ return NULL;
+}
+
+static ScrDyn *nc_listener_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
+ NcListener *l = nc_listener_of(clo);
+ ScrClosure *cb = nc_closure_of(clo);
+ ScrDyn *a0 = argc > 0 ? args[0] : scr_dyn_undefined();
+ switch (l->event) {
+ case NC_HOOK:
+ nc_group_close(l->group);
+ break;
+ case NC_DYN:
+ if (l->dyn != NULL) {
+ ScrDyn *listener = scr_dyn_retain(l->dyn);
+ scr_dyn_release(scr_dyn_call(listener, args, argc, "child listener"));
+ scr_dyn_release(listener);
+ /* An untyped 'close' listener ran in the emit that closed the group. */
+ if (l->on_close && l->group != NULL && l->group->closed && l->dyn != NULL) {
+ ScrDyn *held = l->dyn;
+ l->dyn = NULL;
+ scr_dyn_release(held);
+ }
+ }
+ break;
+ case NC_BRIDGE_WRITE:
+ case NC_BRIDGE_FINAL: {
+ ScrStream *stream = l->stream;
+ if (stream == NULL) break; /* completed already */
+ l->stream = NULL;
+ ScrError *error =
+ a0->kind == SCR_DYN_OBJ ? scr_error_from_dyn(a0) : NULL; /* null or undefined: done */
+ if (l->event == NC_BRIDGE_WRITE) l->stream_ops->write_done(stream, error);
+ else l->stream_ops->final_done(stream, error);
+ l->stream_ops->release(stream);
+ break;
+ }
+ case NC_SEND:
+ if (cb != NULL) {
+ ScrError *error = a0->kind == SCR_DYN_OBJ ? scr_error_from_dyn(a0) : NULL;
+ ((ScrIpcSendFn)l->fn)(cb, error);
+ scr_box_set_ref(clo->caps[1], NULL); /* called once */
+ }
+ break;
+ default:
+ if (cb == NULL) break; /* released when the child closed */
+ switch (l->event) {
+ case SCR_CHILD_EV_EXIT:
+ case SCR_CHILD_EV_CLOSE: {
+ ScrDyn *signal = argc > 1 ? args[1] : scr_dyn_undefined();
+ ((ScrChildExitFn)l->fn)(cb, a0->kind == SCR_DYN_NUM, a0->kind == SCR_DYN_NUM ? a0->v.num : 0,
+ signal->kind == SCR_DYN_STR ? nc_signal_name(signal->v.str) : NULL);
+ break;
+ }
+ case SCR_CHILD_EV_ERROR:
+ case SCR_CHILD_EV_WRITE_ERROR: {
+ ScrError *error =
+ nc_error_of(a0, "an 'error' listener that takes an Error got a value that is not one");
+ if (error == NULL) break;
+ /* The adapters take a message and make the Error; with the payload
+ * set they hand over this one (scr_child_err_thunk_error). */
+ ScrError *outer = scr_event_error_payload;
+ scr_event_error_payload = error;
+ ((ScrChildErrFn)l->fn)(cb, error->message);
+ scr_event_error_payload = outer;
+ scr_error_release(error);
+ break;
+ }
+ case SCR_CHILD_EV_DATA:
+ if (l->fn == (void *)&scr_child_stream_thunk0) scr_child_stream_thunk0(cb, NULL);
+ else if (a0->kind == SCR_DYN_BYTES) ((ScrChildStreamDataFn)l->fn)(cb, a0->v.bytes);
+ else
+ nc_refuse("a 'data' listener that takes a Buffer got a string: an encoding is set on the "
+ "island's stream");
+ break;
+ case SCR_CHILD_EV_DATA_STR:
+ if (a0->kind == SCR_DYN_STR) ((ScrChildStreamDataStrFn)l->fn)(cb, a0->v.str);
+ else
+ nc_refuse("a 'data' listener that takes a string got a Buffer: no encoding is set on the "
+ "island's stream");
+ break;
+ case SCR_CHILD_EV_MESSAGE: {
+ ScrDyn *message = a0->kind == SCR_DYN_STR ? scr_json_parse(a0->v.str)
+ : scr_dyn_retain(scr_dyn_undefined());
+ if (message != NULL) ((ScrIpcMessageFn)l->fn)(cb, message);
+ scr_dyn_release(message);
+ break;
+ }
+ default: /* 'spawn', 'disconnect', 'end', 'drain', 'finish': no argument */
+ ((void (*)(ScrClosure *))cb->fn)(cb);
+ break;
+ }
+ /* A 'close' listener ran in the emit that closed the group. */
+ if (l->event == SCR_CHILD_EV_CLOSE && l->group != NULL && l->group->closed)
+ scr_box_set_ref(clo->caps[1], NULL);
+ break;
+ }
+ scr_closure_release(cb);
+ nc_listener_release(l);
+ if (scr_exc_pending()) return NULL;
+ return scr_dyn_retain(scr_dyn_undefined());
+}
+
+/* ── the handles ──────────────────────────────────────────────────────── */
+enum { NC_CHILD = 1, NC_READABLE, NC_WRITABLE };
+
+typedef struct NcHandle {
+ uint8_t kind;
+ bool stdio; /* a child: its stdio handles were made */
+ ScrDyn *object; /* the island's object (+1) */
+ ScrDyn *child; /* a stream: the island child it belongs to (+1) */
+ NcGroup *group; /* +1, once a listener was registered */
+ ScrChild *face; /* a child: its ScrChild (not owned) */
+ ScrArr *spawnargs; /* a child: +1 once read */
+ struct NcHandle *next; /* a child: the identity table's chain */
+} NcHandle;
+
+/* Island object (its import) -> the ScrChild that stands for it, while the
+ * program holds that ScrChild. */
+enum { NC_BUCKETS = 64 };
+static SCR_TL NcHandle *nc_children[NC_BUCKETS];
+
+static size_t nc_bucket(const void *import) {
+ uintptr_t x = (uintptr_t)import;
+ x ^= x >> 33;
+ x *= 0xff51afd7ed558ccdULL;
+ x ^= x >> 33;
+ return (size_t)x % NC_BUCKETS;
+}
+
+static const void *nc_import(const ScrDyn *object) { return object->v.handle.ptr; }
+
+/* The group of the island child a handle belongs to (borrowed; NULL with
+ * the exception pending): the one its hook holds, made with the hook when
+ * the island child has none yet. */
+static NcGroup *nc_group(NcHandle *h) {
+ if (h->group != NULL) return h->group;
+ ScrDyn *child = h->child != NULL ? h->child : h->object;
+ ScrDyn *hook = nc_island_call("hook", &child, 1);
+ if (hook == NULL) return NULL;
+ if (hook->kind == SCR_DYN_FUNC && hook->v.fn.thunk == nc_listener_thunk && hook->v.fn.clo != NULL) {
+ NcListener *l = nc_listener_of(hook->v.fn.clo);
+ if (l->group != NULL) h->group = nc_group_retain(l->group);
+ nc_listener_release(l);
+ }
+ scr_dyn_release(hook);
+ if (h->group != NULL) return h->group;
+ NcGroup *group = calloc(1, sizeof *group);
+ if (group == NULL) nc_oom();
+ group->rc = 1;
+ ScrDyn *args[2] = {child, nc_listener_new(group, NC_HOOK, NULL, NULL, NULL)};
+ ScrDyn *closed = nc_island_call("setHook", args, 2);
+ scr_dyn_release(args[1]);
+ if (closed == NULL) {
+ nc_group_release(group);
+ return NULL;
+ }
+ group->closed = nc_true(closed);
+ h->group = group;
+ return group;
+}
+
+static const char *nc_event_name(int event) {
+ switch (event) {
+ case SCR_CHILD_EV_SPAWN: return "spawn";
+ case SCR_CHILD_EV_EXIT: return "exit";
+ case SCR_CHILD_EV_CLOSE: return "close";
+ case SCR_CHILD_EV_ERROR:
+ case SCR_CHILD_EV_WRITE_ERROR: return "error";
+ case SCR_CHILD_EV_MESSAGE: return "message";
+ case SCR_CHILD_EV_DISCONNECT: return "disconnect";
+ case SCR_CHILD_EV_DATA:
+ case SCR_CHILD_EV_DATA_STR: return "data";
+ case SCR_CHILD_EV_END: return "end";
+ case SCR_CHILD_EV_DRAIN: return "drain";
+ default: return "finish";
+ }
+}
+
+static ScrDyn *nc_str(const char *text, size_t len) {
+ ScrStr *s = scr_str_new(text, len);
+ ScrDyn *d = scr_dyn_new_str(s);
+ scr_str_release(s);
+ return d;
+}
+
+/* emitter.on / once(event, function). */
+static void nc_register(NcHandle *h, int event, ScrDyn *function, bool once) {
+ ScrDyn *answer;
+ if (event == SCR_CHILD_EV_MESSAGE) {
+ ScrDyn *args[3] = {h->object, function, scr_dyn_new_bool(once)};
+ answer = nc_island_call("onMessage", args, 3);
+ scr_dyn_release(args[2]);
+ } else {
+ const char *name = nc_event_name(event);
+ ScrDyn *args[2] = {nc_str(name, strlen(name)), function};
+ answer = scr_ni_call_method(h->object, once ? "once" : "on", once ? 4 : 2, args, 2);
+ scr_dyn_release(args[0]);
+ }
+ scr_dyn_release(answer);
+}
+
+static void nc_on(void *island, int event, ScrClosure *cb, void *fn, bool once) {
+ NcHandle *h = island;
+ NcGroup *group = nc_group(h);
+ /* After the child's 'close' nothing is delivered (scr_child.c: a listener
+ * registered after settling is released and never fires). */
+ if (group == NULL || group->closed) {
+ scr_closure_release(cb);
+ return;
+ }
+ ScrDyn *function = nc_listener_new(group, event, cb, fn, NULL);
+ nc_register(h, event, function, once);
+ scr_dyn_release(function);
+}
+
+static void nc_on_dyn(void *island, ScrStr *event, ScrDyn *listener) {
+ NcHandle *h = island;
+ NcGroup *group = nc_group(h);
+ if (group == NULL || group->closed) return;
+ NcListener *l;
+ ScrDyn *function = nc_listener_new(group, NC_DYN, NULL, NULL, &l);
+ l->dyn = scr_dyn_retain(listener);
+ l->on_close = event->len == 5 && memcmp(event->data, "close", 5) == 0;
+ ScrDyn *args[2] = {scr_dyn_new_str(event), function};
+ scr_dyn_release(scr_ni_call_method(h->object, "on", 2, args, 2));
+ scr_dyn_release(args[0]);
+ scr_dyn_release(function);
+}
+
+/* child.off(event, listener): the most recent registration of this
+ * function on this emitter, as EventEmitter removes it. The emitter says
+ * which of the functions made for the closure it holds. The removed
+ * function keeps the program's closure: an emit that is under way calls
+ * every listener that was registered when it started, so a listener an
+ * earlier one removes still runs in that emit. The closure is released with
+ * the function (the island's collector frees it once the emitter no longer
+ * holds it) or when the child closes. */
+static void nc_off(void *island, int event, ScrClosure *cb) {
+ NcHandle *h = island;
+ NcGroup *group = nc_group(h);
+ if (group == NULL) return;
+ const char *name = nc_event_name(event);
+ for (NcListener *l = group->first; l != NULL;) {
+ NcListener *next = l->next;
+ ScrClosure *held = l->event == event && !l->removed ? nc_closure_of(l->wrapper) : NULL;
+ bool same = held != NULL && scr_closure_identity_equal(held, cb);
+ scr_closure_release(held);
+ if (same) {
+ nc_listener_retain(l);
+ ScrDyn *args[3] = {h->object, nc_str(name, strlen(name)), nc_listener_value(l)};
+ bool removed = nc_true(nc_island_call("off", args, 3));
+ scr_dyn_release(args[1]);
+ scr_dyn_release(args[2]);
+ if (removed) l->removed = true;
+ /* The island call may have freed listeners: the walk ends here either way
+ * when one was removed, and restarts from the list's head otherwise. */
+ next = removed ? NULL : l->next;
+ nc_listener_release(l);
+ if (scr_exc_pending()) return;
+ }
+ l = next;
+ }
+}
+
+static ScrDyn *nc_get(NcHandle *h, const char *key) {
+ return scr_ni_get_str(h->object, key, strlen(key));
+}
+
+static bool nc_number(void *island, int prop, double *value) {
+ if (prop == SCR_CHILD_PROP_FLOWING) { /* readableFlowing: null, false or true */
+ ScrDyn *flowing = nc_get(island, "readableFlowing");
+ bool known = flowing != NULL && flowing->kind == SCR_DYN_BOOL;
+ if (known) *value = flowing->v.b ? 1 : 0;
+ scr_dyn_release(flowing);
+ return known;
+ }
+ ScrDyn *d = nc_get(island, prop == SCR_CHILD_PROP_PID ? "pid" : "exitCode");
+ bool has = d != NULL && d->kind == SCR_DYN_NUM;
+ if (has) *value = d->v.num;
+ scr_dyn_release(d);
+ return has;
+}
+
+static bool nc_boolean(void *island, int prop) {
+ NcHandle *h = island;
+ if (prop == SCR_CHILD_PROP_CHANNEL) return nc_true(nc_island_call("channel", &h->object, 1));
+ if (prop == SCR_CHILD_PROP_PAUSED) return nc_true(scr_ni_call_method(h->object, "isPaused", 8, NULL, 0));
+ return nc_true(nc_get(h, prop == SCR_CHILD_PROP_KILLED ? "killed"
+ : prop == SCR_CHILD_PROP_CONNECTED ? "connected"
+ : "writable"));
+}
+
+static ScrStr *nc_string(void *island, int prop) {
+ ScrDyn *d = nc_get(island, prop == SCR_CHILD_PROP_SIGNAL_CODE ? "signalCode" : "spawnfile");
+ ScrStr *s = d != NULL && d->kind == SCR_DYN_STR ? scr_str_retain(d->v.str) : NULL;
+ scr_dyn_release(d);
+ return s;
+}
+
+/* child.spawnargs: read once, so repeated reads answer one array, as the
+ * island's property does. */
+static ScrArr *nc_spawnargs(void *island) {
+ NcHandle *h = island;
+ if (h->spawnargs == NULL) {
+ ScrArr *list = scr_arr_new(SCR_ELEM_STR, 0);
+ ScrDyn *text = nc_island_call("spawnargs", &h->object, 1);
+ ScrDyn *items = text != NULL && text->kind == SCR_DYN_STR ? scr_json_parse(text->v.str) : NULL;
+ if (items != NULL && items->kind == SCR_DYN_ARR)
+ for (size_t i = 0; i < items->v.arr.len; i++)
+ if (items->v.arr.items[i]->kind == SCR_DYN_STR)
+ scr_arr_push_ref(list, scr_str_retain(items->v.arr.items[i]->v.str));
+ scr_dyn_release(items);
+ scr_dyn_release(text);
+ h->spawnargs = list;
+ }
+ return scr_arr_retain(h->spawnargs);
+}
+
+static bool nc_kill(void *island, const ScrStr *signal, double signum) {
+ NcHandle *h = island;
+ ScrDyn *arg = signal != NULL ? scr_dyn_new_str((ScrStr *)signal) : scr_dyn_new_num(signum);
+ bool sent = nc_true(scr_ni_call_method(h->object, "kill", 4, &arg, 1));
+ scr_dyn_release(arg);
+ return sent;
+}
+
+static void nc_call(void *island, const char *method) {
+ NcHandle *h = island;
+ scr_dyn_release(scr_ni_call_method(h->object, method, strlen(method), NULL, 0));
+}
+
+static bool nc_send(void *island, ScrStr *json, ScrClosure *cb, ScrIpcSendFn fn) {
+ NcHandle *h = island;
+ ScrDyn *args[3] = {h->object, scr_dyn_new_str(json),
+ cb != NULL ? nc_listener_new(NULL, NC_SEND, cb, (void *)fn, NULL) : NULL};
+ bool sent = nc_true(nc_island_call("send", args, cb != NULL ? 3 : 2));
+ scr_dyn_release(args[1]);
+ scr_dyn_release(args[2]);
+ return sent;
+}
+
+static void nc_set_encoding(void *island, ScrStr *encoding) {
+ NcHandle *h = island;
+ ScrDyn *arg = scr_dyn_new_str(encoding);
+ scr_dyn_release(scr_ni_call_method(h->object, "setEncoding", 11, &arg, 1));
+ scr_dyn_release(arg);
+}
+
+static bool nc_write(void *island, const ScrStr *text, const ScrBytes *bytes) {
+ NcHandle *h = island;
+ ScrDyn *arg = text != NULL ? scr_dyn_new_str((ScrStr *)text) : scr_dyn_new_buffer(bytes);
+ bool more = nc_true(scr_ni_call_method(h->object, "write", 5, &arg, 1));
+ scr_dyn_release(arg);
+ return more;
+}
+
+/* A general stream's write or end into the island's stdin: the island's
+ * callback completes it. A call that throws completes it with that error. */
+static void nc_bridge(NcHandle *h, int event, ScrStream *stream, ScrBytes *chunk,
+ const ScrChildStreamOps *stream_ops) {
+ NcListener *l;
+ ScrDyn *done = nc_listener_new(NULL, event, NULL, NULL, &l);
+ l->stream = stream_ops->retain(stream);
+ l->stream_ops = stream_ops;
+ ScrDyn *args[2] = {chunk != NULL ? scr_dyn_new_buffer(chunk) : done, done};
+ ScrDyn *answer = event == NC_BRIDGE_WRITE ? scr_ni_call_method(h->object, "write", 5, args, 2)
+ : scr_ni_call_method(h->object, "end", 3, args, 1);
+ if (chunk != NULL) scr_dyn_release(args[0]);
+ if (answer == NULL && scr_exc_pending() && l->stream != NULL) {
+ ScrCaught *caught = scr_exc_take();
+ ScrDyn *thrown = scr_caught_to_dyn(caught);
+ scr_caught_release(caught);
+ ScrError *error = thrown != NULL && thrown->kind == SCR_DYN_OBJ ? scr_error_from_dyn(thrown) : NULL;
+ scr_dyn_release(thrown);
+ l->stream = NULL;
+ if (event == NC_BRIDGE_WRITE) stream_ops->write_done(stream, error);
+ else stream_ops->final_done(stream, error);
+ stream_ops->release(stream);
+ }
+ scr_dyn_release(answer);
+ scr_dyn_release(done);
+}
+
+static void nc_bridge_write(void *island, ScrStream *stream, ScrBytes *chunk,
+ const ScrChildStreamOps *stream_ops) {
+ nc_bridge(island, NC_BRIDGE_WRITE, stream, chunk, stream_ops);
+}
+
+static void nc_bridge_final(void *island, ScrStream *stream, const ScrChildStreamOps *stream_ops) {
+ nc_bridge(island, NC_BRIDGE_FINAL, stream, NULL, stream_ops);
+}
+
+static NcHandle *nc_handle_new(int kind, ScrDyn *object /* moves */, ScrDyn *child) {
+ NcHandle *h = calloc(1, sizeof *h);
+ if (h == NULL) nc_oom();
+ h->kind = (uint8_t)kind;
+ h->object = object;
+ h->child = child != NULL ? scr_dyn_retain(child) : NULL;
+ return h;
+}
+
+/* One stdio member of the island child: its handle's island part, or NULL
+ * for null (a slot that is not a pipe). Moves nothing. */
+static NcHandle *nc_stdio_member(NcHandle *h, const char *key, int kind) {
+ ScrDyn *stream = nc_get(h, key);
+ if (stream != NULL && scr_ni_is_handle(stream)) return nc_handle_new(kind, stream, h->object);
+ scr_dyn_release(stream);
+ return NULL;
+}
+
+static void nc_stdio(ScrChild *child) {
+ NcHandle *h = child->island;
+ if (h->stdio) return;
+ h->stdio = true;
+ NcHandle *in = nc_stdio_member(h, "stdin", NC_WRITABLE);
+ NcHandle *out = nc_stdio_member(h, "stdout", NC_READABLE);
+ NcHandle *err = nc_stdio_member(h, "stderr", NC_READABLE);
+ if (in != NULL) child->in_stream = scr_child_writer_island_new(in);
+ if (out != NULL) child->out_stream = scr_child_stream_island_new(out);
+ if (err != NULL) child->err_stream = scr_child_stream_island_new(err);
+}
+
+static void nc_drop(void *island) {
+ NcHandle *h = island;
+ if (h->kind == NC_CHILD) {
+ NcHandle **at = &nc_children[nc_bucket(nc_import(h->object))];
+ while (*at != NULL && *at != h) at = &(*at)->next;
+ if (*at == h) *at = h->next;
+ if (h->spawnargs != NULL) scr_arr_release(h->spawnargs);
+ }
+ nc_group_release(h->group);
+ scr_dyn_release(h->child);
+ scr_dyn_release(h->object);
+ free(h);
+}
+
+static const ScrChildIslandOps nc_ops = {
+ nc_drop, nc_stdio, nc_on, nc_off, nc_on_dyn, nc_number,
+ nc_boolean, nc_string, nc_spawnargs, nc_kill, nc_call, nc_send,
+ nc_set_encoding, nc_write, nc_bridge_write, nc_bridge_final,
+};
+
+/* ── the conversions (dyn.ts, ISLAND_ADOPTED_KINDS) ─────────────────────── */
+
+static bool nc_is_child(const ScrDyn *d) {
+ ScrDyn *arg = (ScrDyn *)d;
+ return nc_true(nc_island_call("isChild", &arg, 1));
+}
+
+/* A dynamic value into a ChildProcess slot (+1): a native child as it is,
+ * an island's ChildProcess adopted, anything else the conversion's
+ * TypeError. */
+ScrChild *scr_ni_child_unbox(const ScrDyn *d, const ScrDynPath *path, const char *want) {
+ if (!scr_ni_dyn_handle(d)) return scr_dyn_handle_unbox(d, SCR_DYNH_CHILD, path, want);
+ size_t bucket = nc_bucket(nc_import(d));
+ for (NcHandle *h = nc_children[bucket]; h != NULL; h = h->next)
+ if (nc_import(h->object) == nc_import(d)) return scr_child_retain(h->face);
+ if (!nc_is_child(d)) {
+ if (!scr_exc_pending()) scr_dyn_check_fail(path, want, d);
+ return NULL;
+ }
+ scr_child_island_ops = &nc_ops;
+ NcHandle *h = nc_handle_new(NC_CHILD, scr_dyn_retain((ScrDyn *)d), NULL);
+ h->face = scr_child_island_new(h);
+ h->next = nc_children[bucket];
+ nc_children[bucket] = h;
+ return h->face;
+}
+
+/* Whether a dynamic value is a ChildProcess (a union's arm test). */
+bool scr_ni_child_is(const ScrDyn *d) {
+ if (d->kind != SCR_DYN_HANDLE) return false;
+ if (d->v.handle.tag == SCR_DYNH_CHILD) return true;
+ if (d->v.handle.tag != SCR_DYNH_NODE_ISLAND) return false;
+ for (NcHandle *h = nc_children[nc_bucket(nc_import(d))]; h != NULL; h = h->next)
+ if (nc_import(h->object) == nc_import(d)) return true;
+ ScrCaught *outer = scr_exc_pending() ? scr_exc_take() : NULL;
+ bool is = nc_is_child(d);
+ if (scr_exc_pending()) scr_exc_clear();
+ if (outer != NULL) {
+ scr_rethrow(outer);
+ scr_caught_release(outer);
+ }
+ return is;
+}
+
+/* A ChildProcess slot's value as a dynamic value (+1): the island's own
+ * object for an adopted child, the native handle otherwise. */
+ScrDyn *scr_ni_child_box(ScrChild *child) {
+ if (child->island != NULL) return scr_dyn_retain(((NcHandle *)child->island)->object);
+ return scr_dyn_new_handle(child, SCR_DYNH_CHILD);
+}
+
+#else /* _WIN32 || __wasi__: no islands */
+
+ScrChild *scr_ni_child_unbox(const ScrDyn *d, const ScrDynPath *path, const char *want) {
+ return scr_dyn_handle_unbox(d, SCR_DYNH_CHILD, path, want);
+}
+
+bool scr_ni_child_is(const ScrDyn *d) {
+ return d->kind == SCR_DYN_HANDLE && d->v.handle.tag == SCR_DYNH_CHILD;
+}
+
+ScrDyn *scr_ni_child_box(ScrChild *child) { return scr_dyn_new_handle(child, SCR_DYNH_CHILD); }
+
+#endif
diff --git a/packages/runtime/src/scr_node_island_events.c b/packages/runtime/src/scr_node_island_events.c
index 49de40cbdef..4f465d9c35f 100644
--- a/packages/runtime/src/scr_node_island_events.c
+++ b/packages/runtime/src/scr_node_island_events.c
@@ -179,4 +179,18 @@ bool scr_node_island_has_exit_code(void) {
return state < 0 ? scr_process_has_exit_code() : state == 1;
}
+/* process.chdir in a program with islands (island-process-state.ts): the
+ * working directory is the process's, and Node caches process.cwd() until
+ * its own process.chdir runs. After the program's change Node changes to
+ * ".", which keeps the directory and drops the cached answer. A Node that
+ * is not loaded yet reads the directory when it starts. */
+void scr_node_island_chdir(ScrStr *dir) {
+ scr_process_chdir(dir);
+ if (scr_exc_pending() || !scr_context_is_main() || !scr_ni_running()) return;
+ scr_dyn_release(scr_ni_helper("processChdir", NULL, 0));
+ /* The program's own change succeeded: a directory that vanished since
+ * leaves Node's cached answer, which its next process.chdir replaces. */
+ if (scr_exc_pending()) scr_exc_clear();
+}
+
#endif
diff --git a/packages/runtime/src/scr_node_island_export.c b/packages/runtime/src/scr_node_island_export.c
index 927fba13280..e03ebbdc1cd 100644
--- a/packages/runtime/src/scr_node_island_export.c
+++ b/packages/runtime/src/scr_node_island_export.c
@@ -1994,6 +1994,20 @@ bool scr_node_island_instanceof_global(ScrDyn *value, ScrStr *name) {
return yes;
}
+/* clearTimeout (which 0), clearInterval (1) or clearImmediate (2) of a
+ * checked-dynamic value that is not a number: a timer island code created
+ * is cleared by Node's own function, and every other value is ignored, as
+ * Node ignores values that are not timers. A handle exists only once Node
+ * is loaded, so this never starts it. */
+void scr_node_island_clear_timer(ScrDyn *value, double which) {
+ if (value == NULL || !scr_ni_is_handle(value)) return;
+ ScrDyn *w = scr_dyn_new_num(which);
+ ScrDyn *args[] = {w, value};
+ ScrDyn *r = scr_ni_helper("clearTimer", args, 2);
+ scr_dyn_release(w);
+ scr_dyn_release(r);
+}
+
/* util.promisify of an island function: Node's own, in the island. */
ScrDyn *scr_node_island_promisify(ScrDyn *fn) {
ScrDyn *args[] = {fn};
diff --git a/packages/runtime/src/scr_node_island_host.cjs b/packages/runtime/src/scr_node_island_host.cjs
index 2670e7ed10f..3ba14f207ff 100644
--- a/packages/runtime/src/scr_node_island_host.cjs
+++ b/packages/runtime/src/scr_node_island_host.cjs
@@ -724,10 +724,21 @@ const islandHelpers = {
const current = process.exitCode;
return current === undefined || current === null ? null : Number(current);
},
+ // The program changed directory with its own system call. Node caches
+ // process.cwd() until its own process.chdir runs: changing to "." keeps
+ // the directory and drops the cached answer.
+ processChdir() {
+ process.chdir(".");
+ },
// The V8 error a native error crosses as (scr_ni_error_link).
same(value) {
return value;
},
+ // clearTimeout (0), clearInterval (1) or clearImmediate (2) of an island
+ // value the program holds: Node's own function.
+ clearTimer(which, timer) {
+ clearFunctions[which](timer);
+ },
};
// Island imports of modules the program shares resolve to their native
@@ -879,6 +890,52 @@ function nextTick(token) {
process.nextTick(onTick, token);
}
+// ── timers across the boundary ───────────────────────────────────────────
+// A timer or immediate the program created is a number in island code, as
+// it is in the program: its id, which starts at 2^52 in a program with
+// islands, a range Node's own timer ids (async ids) do not reach. Node's
+// clear functions accept such a number and clear the program's handle of that
+// kind through the runtime's own clear (scr_loop_hosted_run in scr_async.c,
+// whose job kinds these are), so the runtime releases its callback and its
+// count of live timers stays right; a clear of the other kind's number finds
+// nothing to clear, as in Node. Every other value is Node's to clear.
+const TIMER_ID_BASE = 2 ** 52;
+const JOB_CLEAR_TIMER = 16;
+const JOB_CLEAR_IMMEDIATE = 16 | 8;
+
+function programTimerId(value) {
+ if (typeof value !== "number" && typeof value !== "string") return 0;
+ const id = typeof value === "string" ? Number(value) : value;
+ return Number.isInteger(id) && id > TIMER_ID_BASE && id < 2 * TIMER_ID_BASE ? id : 0;
+}
+
+function clearProgramTimer(job, id) {
+ const offset = id - TIMER_ID_BASE;
+ nativeHost.run(job | (Math.floor(offset / 2 ** 32) << 8), offset >>> 0);
+}
+
+function clearFunction(name, job) {
+ const nodeClear = timers[name];
+ // The wrapper has the name and length of the Node function it replaces.
+ const clear = {
+ [name](timer) {
+ const id = programTimerId(timer);
+ if (id !== 0) return clearProgramTimer(job, id);
+ return Reflect.apply(nodeClear, this, arguments);
+ },
+ }[name];
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, name);
+ timers[name] = clear;
+ if (descriptor !== undefined)
+ Object.defineProperty(globalThis, name, { ...descriptor, value: clear });
+ return clear;
+}
+const clearFunctions = [
+ clearFunction("clearTimeout", JOB_CLEAR_TIMER),
+ clearFunction("clearInterval", JOB_CLEAR_TIMER),
+ clearFunction("clearImmediate", JOB_CLEAR_IMMEDIATE),
+];
+
// ── process state (scope-B NI-3) ─────────────────────────────────────────
// Signal and 'exit' listeners of island code join the runtime's registries
// at their registration position, so one registry orders both sides and
diff --git a/packages/runtime/src/scr_node_island_host.h b/packages/runtime/src/scr_node_island_host.h
index 50d78ca0b92..b417879c9c4 100644
--- a/packages/runtime/src/scr_node_island_host.h
+++ b/packages/runtime/src/scr_node_island_host.h
@@ -731,10 +731,21 @@ static const char scr_node_island_host_js[] =
" const current = process.exitCode;\012"
" return current === undefined || current === null \077 null : Number(current);\012"
" },\012"
+ " // The program changed directory with its own system call. Node caches\012"
+ " // process.cwd() until its own process.chdir runs: changing to \".\" keeps\012"
+ " // the directory and drops the cached answer.\012"
+ " processChdir() {\012"
+ " process.chdir(\".\");\012"
+ " },\012"
" // The V8 error a native error crosses as (scr_ni_error_link).\012"
" same(value) {\012"
" return value;\012"
" },\012"
+ " // clearTimeout (0), clearInterval (1) or clearImmediate (2) of an island\012"
+ " // value the program holds: Node's own function.\012"
+ " clearTimer(which, timer) {\012"
+ " clearFunctions[which](timer);\012"
+ " },\012"
"};\012"
"\012"
"// Island imports of modules the program shares resolve to their native\012"
@@ -886,6 +897,52 @@ static const char scr_node_island_host_js[] =
" process.nextTick(onTick, token);\012"
"}\012"
"\012"
+ "// \342\224\200\342\224\200 timers across the boundary \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
+ "// A timer or immediate the program created is a number in island code, as\012"
+ "// it is in the program: its id, which starts at 2^52 in a program with\012"
+ "// islands, a range Node's own timer ids (async ids) do not reach. Node's\012"
+ "// clear functions accept such a number and clear the program's handle of that\012"
+ "// kind through the runtime's own clear (scr_loop_hosted_run in scr_async.c,\012"
+ "// whose job kinds these are), so the runtime releases its callback and its\012"
+ "// count of live timers stays right; a clear of the other kind's number finds\012"
+ "// nothing to clear, as in Node. Every other value is Node's to clear.\012"
+ "const TIMER_ID_BASE = 2 ** 52;\012"
+ "const JOB_CLEAR_TIMER = 16;\012"
+ "const JOB_CLEAR_IMMEDIATE = 16 | 8;\012"
+ "\012"
+ "function programTimerId(value) {\012"
+ " if (typeof value !== \"number\" && typeof value !== \"string\") return 0;\012"
+ " const id = typeof value === \"string\" \077 Number(value) : value;\012"
+ " return Number.isInteger(id) && id > TIMER_ID_BASE && id < 2 * TIMER_ID_BASE \077 id : 0;\012"
+ "}\012"
+ "\012"
+ "function clearProgramTimer(job, id) {\012"
+ " const offset = id - TIMER_ID_BASE;\012"
+ " nativeHost.run(job | (Math.floor(offset / 2 ** 32) << 8), offset >>> 0);\012"
+ "}\012"
+ "\012"
+ "function clearFunction(name, job) {\012"
+ " const nodeClear = timers[name];\012"
+ " // The wrapper has the name and length of the Node function it replaces.\012"
+ " const clear = {\012"
+ " [name](timer) {\012"
+ " const id = programTimerId(timer);\012"
+ " if (id !== 0) return clearProgramTimer(job, id);\012"
+ " return Reflect.apply(nodeClear, this, arguments);\012"
+ " },\012"
+ " }[name];\012"
+ " const descriptor = Object.getOwnPropertyDescriptor(globalThis, name);\012"
+ " timers[name] = clear;\012"
+ " if (descriptor !== undefined)\012"
+ " Object.defineProperty(globalThis, name, { ...descriptor, value: clear });\012"
+ " return clear;\012"
+ "}\012"
+ "const clearFunctions = [\012"
+ " clearFunction(\"clearTimeout\", JOB_CLEAR_TIMER),\012"
+ " clearFunction(\"clearInterval\", JOB_CLEAR_TIMER),\012"
+ " clearFunction(\"clearImmediate\", JOB_CLEAR_IMMEDIATE),\012"
+ "];\012"
+ "\012"
"// \342\224\200\342\224\200 process state (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
"// Signal and 'exit' listeners of island code join the runtime's registries\012"
"// at their registration position, so one registry orders both sides and\012"
diff --git a/tests/corpus/9880-node-island-child-adopted/lib.cjs b/tests/corpus/9880-node-island-child-adopted/lib.cjs
new file mode 100644
index 00000000000..d4b750fa9d9
--- /dev/null
+++ b/tests/corpus/9880-node-island-child-adopted/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island: a package that starts a child process and hands it back, as a spawn wrapper does.
+const { spawn } = require("node:child_process");
+exports.start = (command, args) => spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9880-node-island-child-adopted/main.ts b/tests/corpus/9880-node-island-child-adopted/main.ts
new file mode 100644
index 00000000000..e6b54779ac6
--- /dev/null
+++ b/tests/corpus/9880-node-island-child-adopted/main.ts
@@ -0,0 +1,35 @@
+// @requires-libnode
+// A child process started by code in a Node island and used natively at the type node:child_process
+// declares. It arrives untyped and is bound to a ChildProcess: the native child stands for the
+// island's process, so its output, its stdin and its events are the island child's.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+function finished(child: ChildProcess): Promise {
+ return new Promise((resolve) => {
+ let out = "";
+ child.stdout?.on("data", (chunk: Buffer) => {
+ out += chunk.toString();
+ });
+ child.on("error", (error) => resolve(`error:${error.message}`));
+ child.on("close", (code) => resolve(`${out.trim()} close:${code}`));
+ });
+}
+
+async function main(): Promise {
+ for (const script of ["echo hello", "cat"]) {
+ try {
+ const child: ChildProcess = lib.start("sh", ["-c", script]);
+ if (script === "cat") child.stdin?.end("from the parent\n");
+ console.log(script, "->", await finished(child));
+ } catch (error) {
+ console.log(script, "-> failed:", (error as Error).message);
+ }
+ }
+ // The same through an assertion.
+ const asserted = lib.start("sh", ["-c", "echo asserted; exit 4"]) as ChildProcess;
+ console.log("asserted ->", await finished(asserted), "exitCode", asserted.exitCode, typeof asserted.pid);
+}
+main();
diff --git a/tests/corpus/9881-node-island-child-typed-crossings/main.ts b/tests/corpus/9881-node-island-child-typed-crossings/main.ts
new file mode 100644
index 00000000000..5d2d7d43eba
--- /dev/null
+++ b/tests/corpus/9881-node-island-child-typed-crossings/main.ts
@@ -0,0 +1,54 @@
+// @requires-libnode
+// The ways an island's child process reaches a native ChildProcess slot where the compiler sees the
+// crossing: the declared result of a package function handed to a typed parameter, kept in a field,
+// the payload of a promise, and the argument of a callback the island calls.
+import type { ChildProcess } from "node:child_process";
+import { each, later, start } from "spawn-pkg";
+
+function finished(child: ChildProcess): Promise {
+ return new Promise((resolve) => {
+ let out = "";
+ child.stdout?.on("data", (chunk: Buffer) => {
+ out += chunk.toString();
+ });
+ child.on("error", (error) => resolve(`error:${error.message}`));
+ child.on("close", (code, signal) => resolve(`${out.trim()} close:${code} ${signal}`));
+ });
+}
+
+class Transport {
+ private process: ChildProcess | undefined;
+ open(script: string): Promise {
+ this.process = start("sh", ["-c", script]);
+ return finished(this.process);
+ }
+ close(): boolean {
+ const sent = this.process?.kill() ?? false;
+ this.process = undefined;
+ return sent;
+ }
+}
+
+async function main(): Promise {
+ // A typed parameter.
+ const child: ChildProcess = start("sh", ["-c", "echo parameter"]);
+ console.log("parameter ->", await finished(child));
+
+ // A field, closed from another method.
+ const transport = new Transport();
+ const done = transport.open("cat");
+ setTimeout(() => console.log("kill sent", transport.close()), 50);
+ console.log("field ->", await done);
+
+ // A promise's payload.
+ const awaited: ChildProcess = await later("sh", ["-c", "echo payload"]);
+ console.log("payload ->", await finished(awaited));
+
+ // A callback's argument.
+ const results: Promise[] = [];
+ each(["echo first", "echo second; exit 2"], (made: ChildProcess, script: string) => {
+ results.push(finished(made).then((line) => `${script} -> ${line}`));
+ });
+ for (const line of await Promise.all(results)) console.log("callback:", line);
+}
+main();
diff --git a/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/index.d.ts b/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/index.d.ts
new file mode 100644
index 00000000000..1f5b429a54e
--- /dev/null
+++ b/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/index.d.ts
@@ -0,0 +1,4 @@
+import type { ChildProcess } from "node:child_process";
+export declare function start(command: string, args: string[]): ChildProcess;
+export declare function later(command: string, args: string[]): Promise;
+export declare function each(scripts: string[], visit: (child: ChildProcess, script: string) => void): void;
diff --git a/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/index.js b/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/index.js
new file mode 100644
index 00000000000..cde59d7a84a
--- /dev/null
+++ b/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/index.js
@@ -0,0 +1,8 @@
+// A package that starts child processes. The program's package.json runs it in a Node island.
+const { spawn } = require("node:child_process");
+exports.start = (command, args) => spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
+exports.later = (command, args) =>
+ new Promise((resolve) => setImmediate(() => resolve(exports.start(command, args))));
+exports.each = (scripts, visit) => {
+ for (const script of scripts) visit(exports.start("sh", ["-c", script]), script);
+};
diff --git a/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/package.json b/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/package.json
new file mode 100644
index 00000000000..9f38975f0b4
--- /dev/null
+++ b/tests/corpus/9881-node-island-child-typed-crossings/node_modules/spawn-pkg/package.json
@@ -0,0 +1 @@
+{ "name": "spawn-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/9881-node-island-child-typed-crossings/package.json b/tests/corpus/9881-node-island-child-typed-crossings/package.json
new file mode 100644
index 00000000000..5238bddefd1
--- /dev/null
+++ b/tests/corpus/9881-node-island-child-typed-crossings/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "node-island-child-typed-crossings",
+ "private": true,
+ "type": "module",
+ "scriptc": { "packages": { "spawn-pkg": "node" } }
+}
diff --git a/tests/corpus/9882-node-island-child-events/lib.cjs b/tests/corpus/9882-node-island-child-events/lib.cjs
new file mode 100644
index 00000000000..aac131a6059
--- /dev/null
+++ b/tests/corpus/9882-node-island-child-events/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (script) => spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9882-node-island-child-events/main.ts b/tests/corpus/9882-node-island-child-events/main.ts
new file mode 100644
index 00000000000..71babfd0131
--- /dev/null
+++ b/tests/corpus/9882-node-island-child-events/main.ts
@@ -0,0 +1,67 @@
+// @requires-libnode
+// The events and the lifecycle members of a child process a Node island started, read natively:
+// 'spawn' first, output before 'exit', 'exit' before 'close', with the code and signal Node reports,
+// and exitCode, signalCode, killed, pid, connected, spawnfile and spawnargs as they are at each
+// point. Only the narrow stream members are used here (on "data" / "end"), so the streams are the
+// child's own handles and not general streams.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+function state(child: ChildProcess): string {
+ return `exitCode=${child.exitCode} signalCode=${child.signalCode} killed=${child.killed} pid=${typeof child.pid} connected=${child.connected}`;
+}
+
+function watch(name: string, child: ChildProcess, after?: (child: ChildProcess) => void): Promise {
+ return new Promise((resolve) => {
+ const log: string[] = [`start ${state(child)}`];
+ let out = "";
+ let err = "";
+ child.on("spawn", () => log.push("spawn"));
+ child.stdout?.on("data", (chunk: Buffer) => {
+ out += chunk.toString();
+ });
+ child.stderr?.on("data", (chunk: Buffer) => {
+ err += chunk.toString();
+ });
+ child.on("error", (error) => log.push(`error:${error.message}`));
+ child.on("exit", (code, signal) => log.push(`exit:${code}:${signal} ${state(child)}`));
+ child.on("close", (code, signal) => {
+ log.push(`close:${code}:${signal} out=${JSON.stringify(out)} err=${JSON.stringify(err)}`);
+ console.log(`${name}:`);
+ for (const line of log) console.log(" " + line);
+ console.log(" spawnfile", child.spawnfile, "spawnargs", JSON.stringify(child.spawnargs));
+ resolve();
+ });
+ if (after) after(child);
+ });
+}
+
+async function main(): Promise {
+ await watch("ok", lib.start("echo hello"));
+ await watch("code", lib.start("echo e >&2; exit 3"));
+ await watch("killed", lib.start("exec sleep 30"), (child) => {
+ setTimeout(() => console.log(" kill ->", child.kill(), "killed", child.killed), 50);
+ });
+ await watch("signal by name", lib.start("exec sleep 30"), (child) => {
+ setTimeout(() => console.log(" kill(SIGKILL) ->", child.kill("SIGKILL")), 50);
+ });
+ await watch("self-terminated", lib.start("kill -TERM $$"));
+ // kill after the exit answers false; an unknown signal name throws Node's error.
+ const done: ChildProcess = lib.start("true");
+ await new Promise((resolve) => done.on("close", () => resolve()));
+ console.log("kill after close ->", done.kill(), "killed", done.killed);
+ try {
+ done.kill("SIGNOPE");
+ } catch (error) {
+ console.log("unknown signal ->", (error as NodeJS.ErrnoException).code, (error as Error).message);
+ }
+ // ref and unref are the island child's: an unref'd child does not keep the program alive.
+ const lingering: ChildProcess = lib.start("exec sleep 30");
+ lingering.unref();
+ lingering.ref();
+ lingering.unref();
+ setTimeout(() => lingering.kill(), 20);
+}
+main();
diff --git a/tests/corpus/9883-node-island-child-spawn-failure/lib.cjs b/tests/corpus/9883-node-island-child-spawn-failure/lib.cjs
new file mode 100644
index 00000000000..54327d76f22
--- /dev/null
+++ b/tests/corpus/9883-node-island-child-spawn-failure/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (command, args) => spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9883-node-island-child-spawn-failure/main.ts b/tests/corpus/9883-node-island-child-spawn-failure/main.ts
new file mode 100644
index 00000000000..f152fc95dd5
--- /dev/null
+++ b/tests/corpus/9883-node-island-child-spawn-failure/main.ts
@@ -0,0 +1,44 @@
+// @requires-libnode
+// A spawn that fails in a Node island, seen natively: 'error' with the island's error (its name,
+// message and code; the same object for every listener), no 'exit', then 'close' with the negative
+// errno, and pid undefined.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+function watch(child: ChildProcess): Promise {
+ return new Promise((resolve) => {
+ const log: string[] = [];
+ let first: Error | undefined;
+ child.on("spawn", () => log.push("spawn"));
+ child.on("error", (error) => {
+ first = error;
+ const e = error as NodeJS.ErrnoException;
+ log.push(`error ${e.name} ${e.message} instanceof Error ${error instanceof Error} code=${e.code}`);
+ });
+ child.on("error", (error) => log.push(`second listener, same error: ${error === first}`));
+ child.on("exit", (code, signal) => log.push(`exit:${code}:${signal}`));
+ child.on("close", (code, signal) => {
+ log.push(`close:${code}:${signal} exitCode=${child.exitCode} pid=${child.pid} killed=${child.killed}`);
+ for (const line of log) console.log(line);
+ console.log("kill ->", child.kill());
+ resolve();
+ });
+ });
+}
+
+async function main(): Promise {
+ const child: ChildProcess = lib.start("/nonexistent/scriptc-island-child", ["a", "b"]);
+ console.log("pid before the event", child.pid, "exitCode", child.exitCode);
+ await watch(child);
+ // The streams of a failed spawn exist and end without data.
+ const again: ChildProcess = lib.start("/nonexistent/scriptc-island-child-2", []);
+ let ended = 0;
+ again.stdout?.on("data", () => console.log("unexpected data"));
+ again.stdout?.on("end", () => ended++);
+ again.on("error", (error: NodeJS.ErrnoException) => console.log("again:", error.code));
+ await new Promise((resolve) => again.on("close", () => resolve()));
+ console.log("stdout ended", ended);
+}
+main();
diff --git a/tests/corpus/9884-node-island-child-identity/lib.cjs b/tests/corpus/9884-node-island-child-identity/lib.cjs
new file mode 100644
index 00000000000..ee3bcc0c552
--- /dev/null
+++ b/tests/corpus/9884-node-island-child-identity/lib.cjs
@@ -0,0 +1,9 @@
+// Runs in the Node island.
+const { spawn, ChildProcess } = require("node:child_process");
+let kept;
+exports.start = (script) => (kept = spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] }));
+exports.kept = () => kept;
+exports.isKept = (value) => value === kept;
+exports.isChild = (value) => value instanceof ChildProcess;
+exports.count = (child, event) => child.listenerCount(event);
+exports.onExit = (child, listener) => child.on("exit", listener);
diff --git a/tests/corpus/9884-node-island-child-identity/main.ts b/tests/corpus/9884-node-island-child-identity/main.ts
new file mode 100644
index 00000000000..5a96a7e220d
--- /dev/null
+++ b/tests/corpus/9884-node-island-child-identity/main.ts
@@ -0,0 +1,44 @@
+// @requires-libnode
+// Identity of a child process a Node island started. The same island process is the same native
+// child each time it crosses, the native child handed back is the island's own object, listeners
+// are the island emitter's (their order with the island's own, removal by function).
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+async function main(): Promise {
+ const raw: unknown = lib.start("cat");
+ const a = raw as ChildProcess;
+ const b: ChildProcess = lib.kept();
+ console.log("crossing twice, same native child:", a === b);
+ const back: unknown = a;
+ console.log("handed back, the island's object:", lib.isKept(back), lib.isChild(back), back === raw);
+ const held: ChildProcess[] = [a];
+ console.log("found in an array of them:", held.includes(b), held.indexOf(b));
+ console.log("streams are one object:", a.stdout === b.stdout, a.stdin === b.stdin, a.stdout === a.stderr);
+
+ // Listeners are the island emitter's: the island counts them, and they run in registration
+ // order with the island's own.
+ const order: string[] = [];
+ const first = (): void => {
+ order.push("native first");
+ };
+ const removed = (): void => {
+ order.push("removed (must not run)");
+ };
+ a.on("exit", first);
+ lib.onExit(raw, () => order.push("island"));
+ a.on("exit", removed);
+ b.on("exit", () => order.push("native last"));
+ console.log("exit listeners the island sees:", lib.count(raw, "exit"));
+ b.off("exit", removed);
+ a.removeListener("exit", removed);
+ console.log("after removal:", lib.count(raw, "exit"));
+ a.on("close", (code) => {
+ console.log("order:", order.join(", "), "close", code);
+ });
+ a.stdin?.end();
+ await new Promise((resolve) => b.on("close", () => resolve()));
+}
+main();
diff --git a/tests/corpus/9885-node-island-child-outlives-reference/lib.cjs b/tests/corpus/9885-node-island-child-outlives-reference/lib.cjs
new file mode 100644
index 00000000000..8e1be7f7b23
--- /dev/null
+++ b/tests/corpus/9885-node-island-child-outlives-reference/lib.cjs
@@ -0,0 +1,6 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (script) => spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] });
+exports.collect = () => {
+ if (typeof globalThis.gc === "function") globalThis.gc();
+};
diff --git a/tests/corpus/9885-node-island-child-outlives-reference/main.ts b/tests/corpus/9885-node-island-child-outlives-reference/main.ts
new file mode 100644
index 00000000000..427f3447bde
--- /dev/null
+++ b/tests/corpus/9885-node-island-child-outlives-reference/main.ts
@@ -0,0 +1,50 @@
+// @requires-libnode
+// A child process that outlives the program's reference to it. The native child is dropped as soon
+// as its listeners are attached; the process keeps running, its output and its events still arrive,
+// and a listener that captures the child keeps working with it.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const lines: string[] = [];
+
+function attach(name: string, script: string): Promise {
+ return new Promise((resolve) => {
+ const child: ChildProcess = lib.start(script);
+ child.stdout?.on("data", (chunk: Buffer) => lines.push(`${name} out ${chunk.toString().trim()}`));
+ child.on("exit", (code) => lines.push(`${name} exit ${code}`));
+ child.on("close", (code) => {
+ lines.push(`${name} close ${code}`);
+ resolve();
+ });
+ });
+}
+
+function attachCapturing(script: string): Promise {
+ return new Promise((resolve) => {
+ const child: ChildProcess = lib.start(script);
+ // The listeners hold the child they belong to.
+ child.stdout?.on("data", (chunk: Buffer) => {
+ if (chunk.toString().includes("ready")) child.stdin?.write("go\n");
+ else lines.push(`capturing out ${chunk.toString().trim()} (exitCode ${child.exitCode})`);
+ });
+ child.on("close", (code) => {
+ lines.push(`capturing close ${code} exitCode ${child.exitCode} killed ${child.killed}`);
+ resolve();
+ });
+ });
+}
+
+async function main(): Promise {
+ const slow = attach("slow", "sleep 0.2; echo late");
+ const fast = attach("fast", "echo early");
+ await fast;
+ // Nothing native names either child now; collect on both sides while the slow one still runs.
+ lib.collect();
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ lib.collect();
+ await slow;
+ await attachCapturing("echo ready; read line; echo got $line");
+ for (const line of lines.sort()) console.log(line);
+}
+main();
diff --git a/tests/corpus/9886-node-island-child-stdin/lib.cjs b/tests/corpus/9886-node-island-child-stdin/lib.cjs
new file mode 100644
index 00000000000..aac131a6059
--- /dev/null
+++ b/tests/corpus/9886-node-island-child-stdin/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (script) => spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9886-node-island-child-stdin/main.ts b/tests/corpus/9886-node-island-child-stdin/main.ts
new file mode 100644
index 00000000000..6b3a040f7ba
--- /dev/null
+++ b/tests/corpus/9886-node-island-child-stdin/main.ts
@@ -0,0 +1,72 @@
+// @requires-libnode
+// stdin of a child process a Node island started, written natively through the child's own writer:
+// strings and bytes, backpressure ('drain' after a write that answered false), end and 'finish',
+// writable, destroy, and string chunks after setEncoding.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+function closed(child: ChildProcess): Promise {
+ return new Promise((resolve) => child.on("close", (code) => resolve(code)));
+}
+
+async function main(): Promise {
+ // Strings and bytes, then end.
+ const upper: ChildProcess = lib.start("tr a-z A-Z");
+ let out = "";
+ upper.stdout?.on("data", (chunk: Buffer) => {
+ out += chunk.toString();
+ });
+ const input = upper.stdin!;
+ console.log("writable", input.writable);
+ console.log("write string", input.write("hello "));
+ console.log("write bytes", input.write(Buffer.from("world\n")));
+ input.on("finish", () => console.log("finish, writable", input.writable));
+ input.end();
+ console.log("close", await closed(upper), JSON.stringify(out));
+
+ // Backpressure: a child that does not read until told to.
+ const slow: ChildProcess = lib.start("sleep 0.3; wc -c");
+ let count = "";
+ slow.stdout?.on("data", (chunk: Buffer) => {
+ count += chunk.toString();
+ });
+ const chunk = "x".repeat(65536);
+ let writes = 0;
+ let refused = 0;
+ const sink = slow.stdin!;
+ const fill = (): void => {
+ while (writes < 8) {
+ writes++;
+ if (!sink.write(chunk)) {
+ refused++;
+ sink.once("drain", fill);
+ return;
+ }
+ }
+ sink.end();
+ };
+ fill();
+ console.log("close", await closed(slow), "bytes", count.trim(), "writes", writes, "refused at least once", refused > 0);
+
+ // String chunks after setEncoding, split across a multibyte character by the child.
+ const text: ChildProcess = lib.start("printf 'gr\\303'; sleep 0.1; printf '\\274n\\n'");
+ const pieces: string[] = [];
+ text.stdout?.setEncoding("utf8");
+ text.stdout?.on("data", (piece: string) => pieces.push(piece));
+ console.log("close", await closed(text), JSON.stringify(pieces.join("")), pieces.every((p) => typeof p === "string"));
+
+ // destroy closes the pipe: the child sees end of input.
+ const cat: ChildProcess = lib.start("cat; echo done");
+ let echoed = "";
+ cat.stdout?.on("data", (piece: Buffer) => {
+ echoed += piece.toString();
+ });
+ cat.stdin?.write("kept\n");
+ setTimeout(() => {
+ cat.stdin?.destroy();
+ }, 50);
+ console.log("close", await closed(cat), JSON.stringify(echoed));
+}
+main();
diff --git a/tests/corpus/9887-node-island-child-unhandled-error/lib.cjs b/tests/corpus/9887-node-island-child-unhandled-error/lib.cjs
new file mode 100644
index 00000000000..7b0979ed29c
--- /dev/null
+++ b/tests/corpus/9887-node-island-child-unhandled-error/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (command) => spawn(command, [], { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9887-node-island-child-unhandled-error/main.ts b/tests/corpus/9887-node-island-child-unhandled-error/main.ts
new file mode 100644
index 00000000000..2c2d28ad72e
--- /dev/null
+++ b/tests/corpus/9887-node-island-child-unhandled-error/main.ts
@@ -0,0 +1,14 @@
+// @exit: 1
+// @requires-libnode
+// A spawn that fails in a Node island with no 'error' listener anywhere: the emitter's unhandled
+// 'error' ends the program with status 1, as under Node, although a native 'close' listener is
+// attached and another child of the island is still running with its stdin open.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const waiting: ChildProcess = lib.start("cat");
+waiting.on("close", () => console.log("unreachable: cat closed"));
+const child: ChildProcess = lib.start("/nonexistent/scriptc-island-child");
+child.on("close", (code) => console.log("unreachable: close", code));
+console.log("started", typeof waiting.pid, typeof child.pid);
diff --git a/tests/corpus/9888-node-island-child-general-streams/lib.cjs b/tests/corpus/9888-node-island-child-general-streams/lib.cjs
new file mode 100644
index 00000000000..aac131a6059
--- /dev/null
+++ b/tests/corpus/9888-node-island-child-general-streams/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (script) => spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9888-node-island-child-general-streams/main.ts b/tests/corpus/9888-node-island-child-general-streams/main.ts
new file mode 100644
index 00000000000..abfc2385cac
--- /dev/null
+++ b/tests/corpus/9888-node-island-child-general-streams/main.ts
@@ -0,0 +1,50 @@
+// @requires-libnode
+// The stdio of a child process a Node island started, used natively as general streams: stdin as a
+// Writable (write with a callback, end with a last chunk, an 'error' listener), stdout and stderr
+// as Readables (pipe into a PassThrough, async iteration, 'close'), as a stdio transport does.
+import type { ChildProcess } from "node:child_process";
+import { PassThrough } from "node:stream";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+function closed(child: ChildProcess): Promise {
+ return new Promise((resolve) => child.on("close", (code) => resolve(code)));
+}
+
+async function main(): Promise {
+ // A line protocol over stdin and stdout.
+ const echo: ChildProcess = lib.start("while read line; do echo \"got $line\"; done; echo eof >&2");
+ const replies: string[] = [];
+ let buffered = "";
+ const input = echo.stdin!;
+ input.on("error", (error) => console.log("stdin error", error.message));
+ echo.stdout?.on("error", (error) => console.log("stdout error", error.message));
+ echo.stdout?.on("data", (chunk: Buffer) => {
+ buffered += chunk.toString();
+ let at = buffered.indexOf("\n");
+ while (at >= 0) {
+ replies.push(buffered.slice(0, at));
+ buffered = buffered.slice(at + 1);
+ at = buffered.indexOf("\n");
+ }
+ if (replies.length === 1) input.write("two\n", (error) => console.log("second write done", error == null));
+ if (replies.length === 2) input.end("three\n");
+ });
+ const errors = new PassThrough();
+ let stderrText = "";
+ errors.on("data", (chunk: Buffer) => {
+ stderrText += chunk.toString();
+ });
+ echo.stderr?.pipe(errors);
+ input.write("one\n");
+ console.log("close", await closed(echo), JSON.stringify(replies), JSON.stringify(stderrText));
+
+ // Async iteration over stdout.
+ const lines: ChildProcess = lib.start("echo a; sleep 0.05; echo b");
+ let text = "";
+ for await (const chunk of lines.stdout!) text += String(chunk);
+ console.log("iterated", JSON.stringify(text), "exitCode", lines.exitCode);
+
+}
+main();
diff --git a/tests/corpus/9889-node-island-child-ipc/lib.cjs b/tests/corpus/9889-node-island-child-ipc/lib.cjs
new file mode 100644
index 00000000000..b84999a4b17
--- /dev/null
+++ b/tests/corpus/9889-node-island-child-ipc/lib.cjs
@@ -0,0 +1,9 @@
+// Runs in the Node island. The child is a Node program with a message channel: it answers every
+// message and leaves when told to. `node` comes from PATH (in the island process.execPath is the
+// compiled program itself).
+const { spawn } = require("node:child_process");
+const CHILD = `process.on("message", (message) => {
+ if (message && message.bye) process.disconnect();
+ else process.send({ echo: message });
+});`;
+exports.echo = () => spawn("node", ["-e", CHILD], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
diff --git a/tests/corpus/9889-node-island-child-ipc/main.ts b/tests/corpus/9889-node-island-child-ipc/main.ts
new file mode 100644
index 00000000000..614b2c02ce6
--- /dev/null
+++ b/tests/corpus/9889-node-island-child-ipc/main.ts
@@ -0,0 +1,29 @@
+// @requires-libnode
+// The message channel of a child a Node island forked, used natively: send, 'message', connected,
+// disconnect and 'disconnect' are the island child's.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+interface Reply {
+ echo: { n: number; text: string };
+}
+
+async function main(): Promise {
+ const child: ChildProcess = lib.echo();
+ console.log("has a channel", "send" in child, "connected", child.connected);
+ const replies: string[] = [];
+ child.on("message", (message) => {
+ const reply = message as Reply;
+ replies.push(`${reply.echo.n}:${reply.echo.text}`);
+ if (reply.echo.n === 1) console.log("send", child.send({ n: 2, text: "again" }));
+ else if (reply.echo.n === 2) child.send({ n: 3, text: "last" }, (error) => console.log("send callback", error === null));
+ else child.send({ bye: true });
+ });
+ child.on("disconnect", () => console.log("disconnect, connected", child.connected));
+ child.send({ n: 1, text: "first" });
+ await new Promise((resolve) => child.on("close", () => resolve()));
+ console.log("replies", replies.join(" "), "exitCode", child.exitCode);
+}
+main();
diff --git a/tests/corpus/9890-node-island-child-off-during-emit/lib.cjs b/tests/corpus/9890-node-island-child-off-during-emit/lib.cjs
new file mode 100644
index 00000000000..a3938407564
--- /dev/null
+++ b/tests/corpus/9890-node-island-child-off-during-emit/lib.cjs
@@ -0,0 +1,5 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = () => spawn("sh", ["-c", "exec sleep 30"], { stdio: ["pipe", "pipe", "pipe"] });
+// Emits 'spawn' on the child once more, synchronously: an emit the program's listeners see again.
+exports.emitSpawn = (child) => child.emit("spawn");
diff --git a/tests/corpus/9890-node-island-child-off-during-emit/main.ts b/tests/corpus/9890-node-island-child-off-during-emit/main.ts
new file mode 100644
index 00000000000..3daa75d2d2b
--- /dev/null
+++ b/tests/corpus/9890-node-island-child-off-during-emit/main.ts
@@ -0,0 +1,101 @@
+// @requires-libnode
+// A listener of a child process a Node island started removes a listener of the same event while
+// the event is being emitted. Node calls every listener that was registered when the emit started:
+// a later sibling an earlier one removes still runs in that emit and in no later one. Each line
+// is three emits of 'spawn' (the child's own, then two the island makes), separated by "|".
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+async function scenario(name: string, arrange: (child: ChildProcess, log: string[]) => void): Promise {
+ const child: ChildProcess = lib.start();
+ const log: string[] = [];
+ arrange(child, log);
+ await new Promise((resolve) => child.on("spawn", () => resolve()));
+ log.push("|");
+ lib.emitSpawn(child);
+ log.push("|");
+ lib.emitSpawn(child);
+ const closed = new Promise((resolve) => child.on("close", () => resolve()));
+ child.kill();
+ await closed;
+ console.log(`${name}: ${log.join(" ")}`);
+}
+
+async function main(): Promise {
+ await scenario("a later sibling", (child, log) => {
+ const b = (): void => {
+ log.push("b");
+ };
+ child.on("spawn", () => {
+ log.push("a");
+ child.off("spawn", b);
+ });
+ child.on("spawn", b);
+ child.on("spawn", () => {
+ log.push("c");
+ });
+ });
+ await scenario("an earlier sibling", (child, log) => {
+ const a = (): void => {
+ log.push("a");
+ };
+ child.on("spawn", a);
+ child.on("spawn", () => {
+ log.push("b");
+ child.off("spawn", a);
+ });
+ });
+ await scenario("itself", (child, log) => {
+ const a = (): void => {
+ log.push("a");
+ child.off("spawn", a);
+ };
+ child.on("spawn", a);
+ child.on("spawn", () => {
+ log.push("b");
+ });
+ });
+ await scenario("removed and registered again", (child, log) => {
+ const b = (): void => {
+ log.push("b");
+ };
+ child.on("spawn", () => {
+ log.push("a");
+ child.off("spawn", b);
+ child.on("spawn", b);
+ });
+ child.on("spawn", b);
+ });
+ await scenario("an emit inside the emit", (child, log) => {
+ let calls = 0;
+ const b = (): void => {
+ log.push("b");
+ };
+ child.on("spawn", () => {
+ log.push("a");
+ child.off("spawn", b);
+ if (++calls === 1) {
+ log.push("(");
+ lib.emitSpawn(child);
+ log.push(")");
+ }
+ });
+ child.on("spawn", b);
+ child.on("spawn", () => {
+ log.push("c");
+ });
+ });
+ await scenario("removed before the emit", (child, log) => {
+ const a = (): void => {
+ log.push("a");
+ };
+ child.on("spawn", a);
+ child.on("spawn", () => {
+ log.push("b");
+ });
+ child.off("spawn", a);
+ });
+}
+main();
diff --git a/tests/corpus/9894-node-island-process-cwd/main.ts b/tests/corpus/9894-node-island-process-cwd/main.ts
new file mode 100644
index 00000000000..0b5c85c0644
--- /dev/null
+++ b/tests/corpus/9894-node-island-process-cwd/main.ts
@@ -0,0 +1,31 @@
+// @requires-libnode
+// The working directory is one state for the program and the package it
+// runs in a Node island. Node caches process.cwd() until its own
+// process.chdir runs, so a directory the program changes to must reach
+// island code too, and the other way round.
+import { mkdtempSync, mkdirSync, realpathSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { basename, join } from "node:path";
+import { chdir, cwdName, resolved } from "process-state";
+
+const root = realpathSync(mkdtempSync(join(tmpdir(), "island-cwd-")));
+mkdirSync(join(root, "first"));
+mkdirSync(join(root, "second"));
+mkdirSync(join(root, "third"));
+const here = (): string => basename(process.cwd());
+
+process.chdir(join(root, "first"));
+// The first island call starts Node, in the directory the program is in.
+console.log(cwdName(), here());
+process.chdir(join(root, "second"));
+console.log(cwdName(), here(), resolved("file.txt"));
+chdir(join(root, "third"));
+console.log(cwdName(), here());
+process.chdir("../first");
+console.log(cwdName(), here(), resolved("file.txt"));
+chdir("../second");
+process.chdir("../third");
+console.log(cwdName(), here());
+
+process.chdir(tmpdir());
+rmSync(root, { recursive: true });
diff --git a/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/index.d.ts b/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/index.d.ts
new file mode 100644
index 00000000000..c1d0e95b1d4
--- /dev/null
+++ b/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/index.d.ts
@@ -0,0 +1,3 @@
+export function cwdName(): string;
+export function chdir(directory: string): void;
+export function resolved(file: string): string;
diff --git a/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/index.js b/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/index.js
new file mode 100644
index 00000000000..dcb6b5fa89e
--- /dev/null
+++ b/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/index.js
@@ -0,0 +1,12 @@
+import { basename, dirname, resolve } from "node:path";
+
+export function cwdName() {
+ return basename(process.cwd());
+}
+export function chdir(directory) {
+ process.chdir(directory);
+}
+// path.resolve reads process.cwd() for a relative path.
+export function resolved(file) {
+ return basename(dirname(resolve(file)));
+}
diff --git a/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/package.json b/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/package.json
new file mode 100644
index 00000000000..378ea37aea7
--- /dev/null
+++ b/tests/corpus/9894-node-island-process-cwd/node_modules/process-state/package.json
@@ -0,0 +1 @@
+{ "name": "process-state", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/9894-node-island-process-cwd/package.json b/tests/corpus/9894-node-island-process-cwd/package.json
new file mode 100644
index 00000000000..f3f1da0bfa5
--- /dev/null
+++ b/tests/corpus/9894-node-island-process-cwd/package.json
@@ -0,0 +1 @@
+{ "name": "island-process-cwd", "private": true, "type": "module", "scriptc": { "packages": { "process-state": "node" } } }
diff --git a/tests/corpus/9895-node-island-child-close-rest-listener/lib.cjs b/tests/corpus/9895-node-island-child-close-rest-listener/lib.cjs
new file mode 100644
index 00000000000..aac131a6059
--- /dev/null
+++ b/tests/corpus/9895-node-island-child-close-rest-listener/lib.cjs
@@ -0,0 +1,3 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = (script) => spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9895-node-island-child-close-rest-listener/main.ts b/tests/corpus/9895-node-island-child-close-rest-listener/main.ts
new file mode 100644
index 00000000000..1cc1cc94ea2
--- /dev/null
+++ b/tests/corpus/9895-node-island-child-close-rest-listener/main.ts
@@ -0,0 +1,38 @@
+// @requires-libnode
+// A listener of a child process a Node island started that takes its arguments as a rest
+// parameter is an untyped listener. On 'close' it runs in the emit that ends the child, in the
+// order it was registered, with the code and the signal, like the typed listeners around it.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+function watch(name: string, script: string): Promise {
+ return new Promise((resolve) => {
+ const child: ChildProcess = lib.start(script);
+ const log: string[] = [];
+ child.on("exit", (...args) => {
+ log.push(`exit rest ${JSON.stringify(args)}`);
+ });
+ child.on("close", (code) => {
+ log.push(`close typed ${code}`);
+ });
+ child.on("close", (...args) => {
+ log.push(`close rest ${JSON.stringify(args)}`);
+ });
+ child.on("close", (...args) => {
+ log.push(`close rest again ${args.length}`);
+ });
+ child.on("close", () => {
+ console.log(`${name}: ${log.join(" | ")}`);
+ resolve();
+ });
+ });
+}
+
+async function main(): Promise {
+ await watch("code", "exit 3");
+ await watch("signal", "kill -TERM $$");
+ await watch("output first", "echo hello");
+}
+main();
diff --git a/tests/corpus/9896-node-island-child-stream-pause/lib.cjs b/tests/corpus/9896-node-island-child-stream-pause/lib.cjs
new file mode 100644
index 00000000000..36618a73b60
--- /dev/null
+++ b/tests/corpus/9896-node-island-child-stream-pause/lib.cjs
@@ -0,0 +1,4 @@
+// Runs in the Node island.
+const { spawn } = require("node:child_process");
+exports.start = () =>
+ spawn("sh", ["-c", "echo one; read x; echo two; read x; echo three"], { stdio: ["pipe", "pipe", "pipe"] });
diff --git a/tests/corpus/9896-node-island-child-stream-pause/main.ts b/tests/corpus/9896-node-island-child-stream-pause/main.ts
new file mode 100644
index 00000000000..8f90349f4f3
--- /dev/null
+++ b/tests/corpus/9896-node-island-child-stream-pause/main.ts
@@ -0,0 +1,45 @@
+// @requires-libnode
+// pause(), resume(), isPaused() and readableFlowing on the stdout of a child process a Node
+// island started are the island stream's own: a paused stream delivers nothing until it is
+// resumed, and the flow state reads as Node's. The child prints a line, waits for a line on its
+// stdin, prints the next one, and so on, so each chunk arrives only after the program asked for it.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+
+const child: ChildProcess = lib.start();
+const out = child.stdout;
+const log: string[] = [];
+let bytes = 0;
+
+function state(): string {
+ return `paused=${out?.isPaused()} flowing=${out?.readableFlowing} bytes=${bytes}`;
+}
+
+if (out) {
+ log.push(`start ${state()}`);
+ out.on("data", (chunk: Buffer) => {
+ bytes += chunk.length;
+ log.push(`data ${JSON.stringify(chunk.toString())} ${state()}`);
+ if (bytes === 4) {
+ out.pause();
+ log.push(`pause ${state()}`);
+ // The child prints its second line now; the paused stream holds it.
+ child.stdin?.write("go\n");
+ setTimeout(() => {
+ log.push(`still paused ${state()}`);
+ out.resume();
+ log.push(`resume ${state()}`);
+ }, 300);
+ } else if (bytes === 8) {
+ child.stdin?.write("go\n");
+ child.stdin?.end();
+ }
+ });
+ log.push(`listening ${state()}`);
+}
+child.on("close", (code) => {
+ log.push(`close ${code} ${state()}`);
+ for (const line of log) console.log(line);
+});
diff --git a/tests/corpus/node-island-timers-clear-across/lib.cjs b/tests/corpus/node-island-timers-clear-across/lib.cjs
new file mode 100644
index 00000000000..69592c59088
--- /dev/null
+++ b/tests/corpus/node-island-timers-clear-across/lib.cjs
@@ -0,0 +1,4 @@
+"use strict";
+exports.start = (ms) => setTimeout(() => console.log("NOT REACHED island timer"), ms);
+exports.startInterval = (ms) => setInterval(() => console.log("island interval"), ms);
+exports.clear = (t) => clearTimeout(t);
diff --git a/tests/corpus/node-island-timers-clear-across/main.mjs b/tests/corpus/node-island-timers-clear-across/main.mjs
new file mode 100644
index 00000000000..5640b5ca9d6
--- /dev/null
+++ b/tests/corpus/node-island-timers-clear-across/main.mjs
@@ -0,0 +1,15 @@
+// @requires-libnode
+// clearTimeout/clearInterval of a timer island code returned, and island
+// clearTimeout of a timer native code created.
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+clearTimeout(lib.start(10));
+const iv = lib.startInterval(50);
+setTimeout(() => clearInterval(iv), 125);
+const mine = setTimeout(() => console.log("NOT REACHED native timer"), 10);
+lib.clear(mine);
+setTimeout(() => {
+ console.log("end");
+ iv.close();
+}, 300);
diff --git a/tests/corpus/node-island-timers-cross-kind-clears/lib.cjs b/tests/corpus/node-island-timers-cross-kind-clears/lib.cjs
new file mode 100644
index 00000000000..8f944d9a7ca
--- /dev/null
+++ b/tests/corpus/node-island-timers-cross-kind-clears/lib.cjs
@@ -0,0 +1,3 @@
+"use strict";
+exports.clearT = (t) => clearTimeout(t);
+exports.clearImm = (i) => clearImmediate(i);
diff --git a/tests/corpus/node-island-timers-cross-kind-clears/main.mjs b/tests/corpus/node-island-timers-cross-kind-clears/main.mjs
new file mode 100644
index 00000000000..f3280c7c5a6
--- /dev/null
+++ b/tests/corpus/node-island-timers-cross-kind-clears/main.mjs
@@ -0,0 +1,16 @@
+// @requires-libnode
+// Island clearTimeout of an Immediate and clearImmediate of a Timeout clear
+// nothing, like Node, even when the program's first timeout and first
+// immediate are live together: their handles must not share a number.
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const keptTimeout = setTimeout(() => console.log("timeout kept"), 20);
+const keptImmediate = setImmediate(() => console.log("immediate kept"));
+lib.clearT(keptImmediate);
+lib.clearImm(keptTimeout);
+console.log("cleared");
+
+// Each clear function still clears its own kind across the boundary.
+lib.clearT(setTimeout(() => console.log("NOT REACHED timeout")));
+lib.clearImm(setImmediate(() => console.log("NOT REACHED immediate")));
diff --git a/tests/corpus/node-island-timers-island-clears/lib.cjs b/tests/corpus/node-island-timers-island-clears/lib.cjs
new file mode 100644
index 00000000000..001c9727b80
--- /dev/null
+++ b/tests/corpus/node-island-timers-island-clears/lib.cjs
@@ -0,0 +1,33 @@
+"use strict";
+const timers = require("node:timers");
+const log = (s) => console.log(s);
+exports.clearT = (t) => clearTimeout(t);
+exports.clearI = (t) => clearInterval(t);
+exports.clearImm = (i) => clearImmediate(i);
+exports.moduleClearT = (t) => timers.clearTimeout(t);
+exports.moduleClearI = (t) => timers.clearInterval(t);
+exports.moduleClearImm = (i) => timers.clearImmediate(i);
+exports.clearAsString = (t) => clearTimeout(String(t));
+exports.clearAll = (list) => {
+ for (const t of list) clearTimeout(t);
+};
+// The island's own timers keep Node's primitive ids: clearing one by its
+// number clears it and nothing else.
+exports.ownByNumber = () => {
+ const t = setTimeout(() => log("NOT REACHED island timer cleared by its number"), 10);
+ const id = +t;
+ clearTimeout(id);
+ const s = setTimeout(() => log("NOT REACHED island timer cleared by its string"), 10);
+ clearTimeout(`${s[Symbol.toPrimitive]()}`);
+ return typeof id;
+};
+exports.sameFunctions = () =>
+ [
+ clearTimeout === timers.clearTimeout,
+ clearInterval === timers.clearInterval,
+ clearImmediate === timers.clearImmediate,
+ clearTimeout.name,
+ clearInterval.name,
+ clearImmediate.name,
+ clearTimeout.length,
+ ].join(" ");
diff --git a/tests/corpus/node-island-timers-island-clears/main.mjs b/tests/corpus/node-island-timers-island-clears/main.mjs
new file mode 100644
index 00000000000..cf3f9ed812c
--- /dev/null
+++ b/tests/corpus/node-island-timers-island-clears/main.mjs
@@ -0,0 +1,55 @@
+// @requires-libnode
+// Island code clears timers the program created: the global functions and
+// the node:timers ones, a timeout, an interval and an immediate, from the
+// main body and from inside the timer's own callback, a list of them, and a
+// handle that already fired.
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const log = (s) => console.log(s);
+
+log(lib.sameFunctions());
+log(`island numbers are ${lib.ownByNumber()}s`);
+
+lib.clearT(setTimeout(() => log("NOT REACHED timeout"), 10));
+lib.clearI(setTimeout(() => log("NOT REACHED timeout cleared with clearInterval"), 10));
+lib.moduleClearT(setTimeout(() => log("NOT REACHED module timeout"), 10));
+lib.clearAsString(setTimeout(() => log("NOT REACHED string"), 10));
+lib.clearImm(setImmediate(() => log("NOT REACHED immediate")));
+lib.moduleClearImm(setImmediate(() => log("NOT REACHED module immediate")));
+
+// clearTimeout does not clear an immediate, and clearImmediate does not
+// clear a timeout.
+lib.clearT(setImmediate(() => log("immediate kept")));
+lib.clearImm(setTimeout(() => log("timeout kept"), 10));
+
+let ticks = 0;
+const interval = setInterval(() => {
+ log(`native interval tick ${++ticks}`);
+ if (ticks === 3) lib.clearI(interval);
+}, 100);
+
+// An interval stopped by island code from a timer of the program.
+let other = 0;
+const stopped = setInterval(() => log(`second interval tick ${++other}`), 250);
+setTimeout(() => lib.moduleClearI(stopped), 560);
+
+const list = [];
+for (let i = 0; i < 4; i++) list.push(setTimeout(() => log(`NOT REACHED list ${i}`), 30));
+const survivor = setTimeout(() => log("survivor fires"), 650);
+lib.clearAll(list);
+
+// A handle that already fired clears nothing; one that is pending clears.
+const fired = setTimeout(() => log("fired once"), 50);
+const pending = setTimeout(() => log("NOT REACHED pending"), 850);
+setTimeout(() => {
+ lib.clearT(fired);
+ lib.clearT(pending);
+ log("cleared after firing");
+}, 750);
+
+const self = setTimeout(() => {
+ lib.clearT(self);
+ log("cleared itself");
+}, 150);
+setTimeout(() => log("end"), 900);
diff --git a/tests/corpus/node-island-timers-methods/lib.cjs b/tests/corpus/node-island-timers-methods/lib.cjs
new file mode 100644
index 00000000000..ed2ca031fa0
--- /dev/null
+++ b/tests/corpus/node-island-timers-methods/lib.cjs
@@ -0,0 +1,7 @@
+"use strict";
+const log = (s) => console.log(s);
+exports.timeout = (ms, label) => setTimeout(() => log(`island timeout ${label}`), ms);
+exports.interval = (ms, label) => setInterval(() => log(`island interval ${label}`), ms);
+exports.immediate = (label) => setImmediate(() => log(`island immediate ${label}`));
+exports.hasRef = (t) => t.hasRef();
+exports.clearById = (id) => clearTimeout(id);
diff --git a/tests/corpus/node-island-timers-methods/main.mjs b/tests/corpus/node-island-timers-methods/main.mjs
new file mode 100644
index 00000000000..24e152e3d4d
--- /dev/null
+++ b/tests/corpus/node-island-timers-methods/main.mjs
@@ -0,0 +1,40 @@
+// @requires-libnode
+// The program drives an island's Timeout and Immediate through the object:
+// hasRef, unref, ref, refresh, close and the primitive id.
+// The process ends although an unref'd island interval is still armed.
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const log = (s) => console.log(s);
+
+const t = lib.timeout(200, "refreshed");
+log(`hasRef ${t.hasRef()}`);
+log(`unref returns the timer ${t.unref() === t}`);
+log(`hasRef ${t.hasRef()} island agrees ${lib.hasRef(t) === false}`);
+log(`ref returns the timer ${t.ref() === t} hasRef ${t.hasRef()}`);
+setTimeout(() => {
+ log("refresh");
+ t.refresh();
+}, 60);
+setTimeout(() => log("native timer"), 150);
+
+const closed = lib.timeout(10, "NOT REACHED closed");
+log(`close returns the timer ${closed.close() === closed}`);
+
+// An island timer's primitive id is Node's: a number the island's own
+// clearTimeout accepts.
+const byId = lib.timeout(10, "NOT REACHED cleared by its id");
+const id = Number(byId);
+log(`id is a ${typeof id}, ${id === +byId}`);
+lib.clearById(id);
+
+const immediate = lib.immediate("runs");
+log(`immediate hasRef ${immediate.hasRef()}`);
+immediate.unref();
+log(`immediate hasRef ${immediate.hasRef()}`);
+immediate.ref();
+
+const background = lib.interval(1000, "NOT REACHED background");
+background.unref();
+log(`background hasRef ${background.hasRef()}`);
+setTimeout(() => log("end"), 400);
diff --git a/tests/corpus/node-island-timers-native-clears/lib.cjs b/tests/corpus/node-island-timers-native-clears/lib.cjs
new file mode 100644
index 00000000000..98b7f532f83
--- /dev/null
+++ b/tests/corpus/node-island-timers-native-clears/lib.cjs
@@ -0,0 +1,8 @@
+"use strict";
+const log = (s) => console.log(s);
+exports.timeout = (ms, label) => setTimeout(() => log(`island timeout ${label}`), ms);
+exports.interval = (ms, fn) => setInterval(fn, ms);
+exports.immediate = (label) => setImmediate(() => log(`island immediate ${label}`));
+exports.fromModule = (ms, label) =>
+ require("node:timers").setTimeout(() => log(`island timeout ${label}`), ms);
+exports.destroyed = (t) => t._destroyed;
diff --git a/tests/corpus/node-island-timers-native-clears/main.mjs b/tests/corpus/node-island-timers-native-clears/main.mjs
new file mode 100644
index 00000000000..eb50093a36c
--- /dev/null
+++ b/tests/corpus/node-island-timers-native-clears/main.mjs
@@ -0,0 +1,52 @@
+// @requires-libnode
+// clearTimeout, clearInterval and clearImmediate of timers island code
+// created, called by the program: before they fire, from inside a callback,
+// after they fired, with the other clear function, and through values that
+// are not timers.
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const log = (s) => console.log(s);
+
+const cleared = lib.timeout(15, "NOT REACHED cleared");
+clearTimeout(cleared);
+log(`destroyed ${lib.destroyed(cleared)}`);
+clearTimeout(cleared);
+
+const byInterval = lib.timeout(15, "NOT REACHED cleared with clearInterval");
+clearInterval(byInterval);
+
+const fromModule = lib.fromModule(15, "NOT REACHED module");
+clearTimeout(fromModule);
+
+const immediate = lib.immediate("NOT REACHED cleared");
+clearImmediate(immediate);
+
+// clearTimeout does not clear an Immediate, as in Node.
+const kept = lib.immediate("kept");
+clearTimeout(kept);
+
+let ticks = 0;
+const interval = lib.interval(100, () => {
+ log(`island interval tick ${++ticks}`);
+ if (ticks === 3) clearInterval(interval);
+});
+
+const late = lib.timeout(150, "fires");
+setTimeout(() => {
+ clearTimeout(late);
+ log(`late destroyed ${lib.destroyed(late)}`);
+}, 450);
+
+// An interval stopped from a timer of the program, with clearTimeout.
+let stops = 0;
+const stoppedByTimer = lib.interval(250, () => log(`second interval tick ${++stops}`));
+setTimeout(() => clearTimeout(stoppedByTimer), 560);
+
+// Values that are not timers clear nothing.
+for (const value of [lib, {}, "text", true, [], () => {}]) {
+ clearTimeout(value);
+ clearInterval(value);
+ clearImmediate(value);
+}
+setTimeout(() => log("end"), 700);
diff --git a/tests/corpus/node-island-timers-promises/lib.cjs b/tests/corpus/node-island-timers-promises/lib.cjs
new file mode 100644
index 00000000000..fe7a9657f7d
--- /dev/null
+++ b/tests/corpus/node-island-timers-promises/lib.cjs
@@ -0,0 +1,23 @@
+"use strict";
+const timersPromises = require("node:timers/promises");
+const log = (s) => console.log(s);
+exports.sleep = (ms, value) => timersPromises.setTimeout(ms, value);
+exports.soon = (value) => timersPromises.setImmediate(value);
+exports.abortable = (ms) => {
+ const controller = new AbortController();
+ const promise = timersPromises.setTimeout(ms, "NOT REACHED", { signal: controller.signal });
+ return { promise, abort: () => controller.abort() };
+};
+exports.awaitNative = async (promise, label) => {
+ const value = await promise;
+ log(`island awaited ${label}: ${value}`);
+ return `island saw ${value}`;
+};
+exports.ticks = async (ms, n) => {
+ const seen = [];
+ for await (const value of timersPromises.setInterval(ms, "tick")) {
+ seen.push(value);
+ if (seen.length === n) break;
+ }
+ return seen.join(",");
+};
diff --git a/tests/corpus/node-island-timers-promises/main.mjs b/tests/corpus/node-island-timers-promises/main.mjs
new file mode 100644
index 00000000000..318b91a2e56
--- /dev/null
+++ b/tests/corpus/node-island-timers-promises/main.mjs
@@ -0,0 +1,37 @@
+// @requires-libnode
+// timers/promises on both sides: the program awaits the island's timer
+// promises (a value, an immediate, an aborted one), island code awaits the
+// program's, and both keep one order.
+import { setImmediate as soon, setTimeout as sleep } from "node:timers/promises";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const log = (s) => console.log(s);
+
+async function main() {
+ log(`island sleep: ${await lib.sleep(5, "five")}`);
+ log(`island immediate: ${await lib.soon("soon")}`);
+ log(await lib.awaitNative(sleep(5, "native five"), "a native sleep"));
+ log(await lib.awaitNative(soon("native soon"), "a native immediate"));
+
+ const order = [];
+ await Promise.all([
+ lib.sleep(100, "island 100").then((v) => order.push(v)),
+ sleep(50, "native 50").then((v) => order.push(v)),
+ lib.sleep(150, "island 150").then((v) => order.push(v)),
+ sleep(200, "native 200").then((v) => order.push(v)),
+ ]);
+ log(order.join(", "));
+
+ const pending = lib.abortable(1000);
+ setTimeout(() => pending.abort(), 5);
+ try {
+ await pending.promise;
+ } catch (e) {
+ log(`${e.name} ${e.code}`);
+ }
+ log(`island interval: ${await lib.ticks(3, 3)}`);
+ log("end");
+}
+
+main();
diff --git a/tests/corpus/node-island-timers-typed-package/main.ts b/tests/corpus/node-island-timers-typed-package/main.ts
new file mode 100644
index 00000000000..56a3e142d0d
--- /dev/null
+++ b/tests/corpus/node-island-timers-typed-package/main.ts
@@ -0,0 +1,25 @@
+// @requires-libnode
+// A package that runs in a Node island and declares its timers: the program
+// clears the island's timers and the island clears the program's.
+import { cancel, cancelSoon, every, soon, start } from "timer-pkg";
+
+cancel(setTimeout(() => console.log("NOT REACHED native timeout"), 10));
+cancelSoon(setImmediate(() => console.log("NOT REACHED native immediate")));
+let own = 0;
+const mine = setInterval(() => {
+ console.log(`native interval tick ${++own}`);
+ if (own === 2) cancel(mine);
+}, 100);
+
+const t = start(10, "NOT REACHED island timeout");
+clearTimeout(t);
+const i = soon("NOT REACHED island immediate");
+clearImmediate(i);
+let ticks = 0;
+const interval = every(150, () => {
+ console.log(`island interval tick ${++ticks}`);
+ if (ticks === 2) clearInterval(interval);
+});
+const kept = start(50, "fires");
+console.log(`hasRef ${kept.hasRef()}`);
+setTimeout(() => console.log("end"), 500);
diff --git a/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/index.d.ts b/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/index.d.ts
new file mode 100644
index 00000000000..4a328791f51
--- /dev/null
+++ b/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/index.d.ts
@@ -0,0 +1,6 @@
+export declare function start(ms: number, label: string): ReturnType;
+export declare function every(ms: number, fn: () => void): ReturnType;
+export declare function soon(label: string): ReturnType;
+export declare function cancel(t: ReturnType): void;
+export declare function cancelSoon(i: ReturnType): void;
+export declare function kind(t: unknown): string;
diff --git a/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/index.js b/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/index.js
new file mode 100644
index 00000000000..b5454067c17
--- /dev/null
+++ b/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/index.js
@@ -0,0 +1,8 @@
+"use strict";
+// Timers the island owns, and island code that clears the program's.
+exports.start = (ms, label) => setTimeout(() => console.log(`island timeout ${label}`), ms);
+exports.every = (ms, fn) => setInterval(fn, ms);
+exports.soon = (label) => setImmediate(() => console.log(`island immediate ${label}`));
+exports.cancel = (t) => clearTimeout(t);
+exports.cancelSoon = (i) => clearImmediate(i);
+exports.kind = (t) => typeof t;
diff --git a/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/package.json b/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/package.json
new file mode 100644
index 00000000000..6aacd04f5bf
--- /dev/null
+++ b/tests/corpus/node-island-timers-typed-package/node_modules/timer-pkg/package.json
@@ -0,0 +1 @@
+{ "name": "timer-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/corpus/node-island-timers-typed-package/package.json b/tests/corpus/node-island-timers-typed-package/package.json
new file mode 100644
index 00000000000..42e2f09673c
--- /dev/null
+++ b/tests/corpus/node-island-timers-typed-package/package.json
@@ -0,0 +1 @@
+{ "name": "node-island-timers-typed-package", "private": true, "type": "module", "scriptc": { "packages": { "timer-pkg": "node" } } }
diff --git a/tests/corpus/node-island-timers-typescript/lib.cjs b/tests/corpus/node-island-timers-typescript/lib.cjs
new file mode 100644
index 00000000000..a39663ec747
--- /dev/null
+++ b/tests/corpus/node-island-timers-typescript/lib.cjs
@@ -0,0 +1,8 @@
+"use strict";
+const log = (s) => console.log(s);
+exports.timeout = (ms, label) => setTimeout(() => log(`island timeout ${label}`), ms);
+exports.interval = (ms, fn) => setInterval(fn, ms);
+exports.immediate = (label) => setImmediate(() => log(`island immediate ${label}`));
+exports.clearT = (t) => clearTimeout(t);
+exports.clearI = (t) => clearInterval(t);
+exports.clearImm = (i) => clearImmediate(i);
diff --git a/tests/corpus/node-island-timers-typescript/main.ts b/tests/corpus/node-island-timers-typescript/main.ts
new file mode 100644
index 00000000000..35c7844ef03
--- /dev/null
+++ b/tests/corpus/node-island-timers-typescript/main.ts
@@ -0,0 +1,30 @@
+// @requires-libnode
+// The same crossings from TypeScript: an island's values are untyped, and
+// the program's own timers are typed.
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./lib.cjs");
+const log = (s: string): void => console.log(s);
+
+clearTimeout(lib.timeout(10, "NOT REACHED timeout"));
+clearImmediate(lib.immediate("NOT REACHED immediate"));
+let ticks = 0;
+const islandInterval = lib.interval(100, () => {
+ log(`island interval tick ${++ticks}`);
+ if (ticks === 2) clearInterval(islandInterval);
+});
+
+const mine: ReturnType = setTimeout(() => log("NOT REACHED native timeout"), 10);
+lib.clearT(mine);
+const soon: ReturnType = setImmediate(() => log("NOT REACHED native immediate"));
+lib.clearImm(soon);
+let own = 0;
+const nativeInterval: ReturnType = setInterval(() => {
+ log(`native interval tick ${++own}`);
+ if (own === 2) lib.clearI(nativeInterval);
+}, 140);
+let maybe: ReturnType | undefined = setTimeout(() => log("NOT REACHED optional"), 10);
+lib.clearT(maybe);
+maybe = undefined;
+clearTimeout(maybe);
+setTimeout(() => log("end"), 400);
diff --git a/tests/corpus/timers-cross-kind-clears.ts b/tests/corpus/timers-cross-kind-clears.ts
new file mode 100644
index 00000000000..183b8e4c7ff
--- /dev/null
+++ b/tests/corpus/timers-cross-kind-clears.ts
@@ -0,0 +1,20 @@
+// clearTimeout of an Immediate and clearImmediate of a Timeout clear
+// nothing, like Node, even when both were created before either ran: each
+// handle number names one object, not a timeout and an immediate at once.
+const keptTimeout = setTimeout(() => console.log("timeout kept"), 20);
+const keptImmediate = setImmediate(() => console.log("immediate kept"));
+clearTimeout(keptImmediate as unknown as ReturnType);
+clearImmediate(keptTimeout as unknown as ReturnType);
+console.log("cleared");
+
+// The same pair in the other creation order.
+const laterImmediate = setImmediate(() => console.log("later immediate kept"));
+const laterTimeout = setTimeout(() => console.log("later timeout kept"), 10);
+clearImmediate(laterTimeout as unknown as ReturnType);
+clearTimeout(laterImmediate as unknown as ReturnType);
+
+// Each clear function still clears its own kind.
+const clearedTimeout = setTimeout(() => console.log("NOT REACHED timeout"));
+const clearedImmediate = setImmediate(() => console.log("NOT REACHED immediate"));
+clearTimeout(clearedTimeout);
+clearImmediate(clearedImmediate);
diff --git a/tests/diagnostics/node-island-timer-native-use/main.ts b/tests/diagnostics/node-island-timer-native-use/main.ts
new file mode 100644
index 00000000000..71abb93e78b
--- /dev/null
+++ b/tests/diagnostics/node-island-timer-native-use/main.ts
@@ -0,0 +1,9 @@
+// A timer created by island code stays the island's Timeout object. The
+// program's own timer handles are numbers, so a slot typed as a timer
+// cannot hold it: the assignment is refused at the island boundary. A const
+// initialized with the island's timer keeps the handle and may be cleared.
+import { start } from "timer-pkg";
+
+let pending: ReturnType | undefined;
+pending = start(10, "stored in a typed slot");
+clearTimeout(pending);
diff --git a/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/index.d.ts b/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/index.d.ts
new file mode 100644
index 00000000000..4a328791f51
--- /dev/null
+++ b/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/index.d.ts
@@ -0,0 +1,6 @@
+export declare function start(ms: number, label: string): ReturnType;
+export declare function every(ms: number, fn: () => void): ReturnType;
+export declare function soon(label: string): ReturnType;
+export declare function cancel(t: ReturnType): void;
+export declare function cancelSoon(i: ReturnType): void;
+export declare function kind(t: unknown): string;
diff --git a/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/index.js b/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/index.js
new file mode 100644
index 00000000000..b5454067c17
--- /dev/null
+++ b/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/index.js
@@ -0,0 +1,8 @@
+"use strict";
+// Timers the island owns, and island code that clears the program's.
+exports.start = (ms, label) => setTimeout(() => console.log(`island timeout ${label}`), ms);
+exports.every = (ms, fn) => setInterval(fn, ms);
+exports.soon = (label) => setImmediate(() => console.log(`island immediate ${label}`));
+exports.cancel = (t) => clearTimeout(t);
+exports.cancelSoon = (i) => clearImmediate(i);
+exports.kind = (t) => typeof t;
diff --git a/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/package.json b/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/package.json
new file mode 100644
index 00000000000..6aacd04f5bf
--- /dev/null
+++ b/tests/diagnostics/node-island-timer-native-use/node_modules/timer-pkg/package.json
@@ -0,0 +1 @@
+{ "name": "timer-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
diff --git a/tests/diagnostics/node-island-timer-native-use/package.json b/tests/diagnostics/node-island-timer-native-use/package.json
new file mode 100644
index 00000000000..e4b1821a357
--- /dev/null
+++ b/tests/diagnostics/node-island-timer-native-use/package.json
@@ -0,0 +1 @@
+{ "name": "node-island-timer-native-use", "private": true, "type": "module", "scriptc": { "packages": { "timer-pkg": "node" } } }
diff --git a/tests/fixtures/node-island/gc-children.cjs b/tests/fixtures/node-island/gc-children.cjs
new file mode 100644
index 00000000000..c74115a1363
--- /dev/null
+++ b/tests/fixtures/node-island/gc-children.cjs
@@ -0,0 +1,11 @@
+// Runs in the Node island: starts child processes for gc-children.ts.
+const { spawn } = require("node:child_process");
+const v8 = require("node:v8");
+const vm = require("node:vm");
+v8.setFlagsFromString("--expose-gc");
+const gc = vm.runInNewContext("gc");
+v8.setFlagsFromString("--no-expose-gc");
+exports.start = (script) => spawn("sh", ["-c", script], { stdio: ["pipe", "pipe", "pipe"] });
+// A full V8 collection, so the test reads what is held and not what the last
+// scheduled collection happened to leave.
+exports.collect = () => gc();
diff --git a/tests/fixtures/node-island/gc-children.ts b/tests/fixtures/node-island/gc-children.ts
new file mode 100644
index 00000000000..4f837bfdf9b
--- /dev/null
+++ b/tests/fixtures/node-island/gc-children.ts
@@ -0,0 +1,51 @@
+// Child processes a Node island starts, each used natively through a typed ChildProcess whose
+// listeners capture it, then dropped. node-island-gc.test.ts runs this for two iteration counts
+// and compares what stays alive: nothing may grow with the number of children that came and went.
+import type { ChildProcess } from "node:child_process";
+import { requireModule } from "@scriptc/node";
+
+const lib = requireModule("./gc-children.cjs");
+const total = Number(process.argv[2] ?? "200");
+let bytes = 0;
+let closed = 0;
+let codes = 0;
+
+function one(i: number): Promise {
+ return new Promise((resolve) => {
+ const child: ChildProcess = lib.start(`echo ${i}; exit ${i % 3}`);
+ // Each listener holds the child it is registered on: a cycle through both heaps.
+ child.stdout?.on("data", (chunk: Buffer) => {
+ bytes += chunk.length + (child.killed ? 1 : 0);
+ });
+ child.on("exit", (code) => {
+ codes += (code ?? 0) + (child.exitCode ?? 0);
+ });
+ child.on("close", () => {
+ closed += child.killed ? 0 : 1;
+ resolve();
+ });
+ });
+}
+
+async function main(): Promise {
+ for (let done = 0; done < total; done += 20) {
+ const batch: Promise[] = [];
+ for (let i = done; i < Math.min(total, done + 20); i++) batch.push(one(i));
+ await Promise.all(batch);
+ }
+ // One child the program keeps, which must stay usable.
+ const kept: ChildProcess = lib.start("cat");
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ kept.stdin?.end();
+ await new Promise((resolve) => kept.on("close", () => resolve()));
+ // Full collections with turns between them: Node lets go of a child a few turns after its
+ // 'close', a collection then frees the island's child object with its listener functions, and
+ // the next one what those held.
+ for (let pass = 0; pass < 3; pass++) {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ lib.collect();
+ }
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ console.log("closed", closed, "bytes", bytes, "codes", codes, "kept", kept.exitCode);
+}
+main();
diff --git a/tests/harness/__snapshots__/node-island-timer-native-use/main.ts.txt b/tests/harness/__snapshots__/node-island-timer-native-use/main.ts.txt
new file mode 100644
index 00000000000..fac001fbc21
--- /dev/null
+++ b/tests/harness/__snapshots__/node-island-timer-native-use/main.ts.txt
@@ -0,0 +1,8 @@
+node-island-timer-native-use/main.ts:8:11 - error SC8010: an island value of type 'Timeout' used as a native value (a timer created by island code stays the island's object: keep it in a const, clear it, call its methods or pass it back to the island) cannot cross into or out of a Node island yet
+
+ 7 | let pending: ReturnType | undefined;
+ 8 | pending = start(10, "stored in a typed slot");
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 9 | clearTimeout(pending);
+
+ hint: pass primitives, strings, island objects or fresh object and array literals of those; callbacks, native objects and promises cross from NI-5 on
\ No newline at end of file
diff --git a/tests/harness/corpus-costs/9880-node-island-child-adopted/main.ts.json b/tests/harness/corpus-costs/9880-node-island-child-adopted/main.ts.json
new file mode 100644
index 00000000000..c264f910d98
--- /dev/null
+++ b/tests/harness/corpus-costs/9880-node-island-child-adopted/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1988, "san": 11323 }
diff --git a/tests/harness/corpus-costs/9881-node-island-child-typed-crossings/main.ts.json b/tests/harness/corpus-costs/9881-node-island-child-typed-crossings/main.ts.json
new file mode 100644
index 00000000000..b930a391f8f
--- /dev/null
+++ b/tests/harness/corpus-costs/9881-node-island-child-typed-crossings/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 2958, "san": 5753 }
diff --git a/tests/harness/corpus-costs/9882-node-island-child-events/main.ts.json b/tests/harness/corpus-costs/9882-node-island-child-events/main.ts.json
new file mode 100644
index 00000000000..98f2d3244d3
--- /dev/null
+++ b/tests/harness/corpus-costs/9882-node-island-child-events/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 2436, "san": 5388 }
diff --git a/tests/harness/corpus-costs/9883-node-island-child-spawn-failure/main.ts.json b/tests/harness/corpus-costs/9883-node-island-child-spawn-failure/main.ts.json
new file mode 100644
index 00000000000..4d391c0d8b0
--- /dev/null
+++ b/tests/harness/corpus-costs/9883-node-island-child-spawn-failure/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1801, "san": 4184 }
diff --git a/tests/harness/corpus-costs/9884-node-island-child-identity/main.ts.json b/tests/harness/corpus-costs/9884-node-island-child-identity/main.ts.json
new file mode 100644
index 00000000000..4772506eb1c
--- /dev/null
+++ b/tests/harness/corpus-costs/9884-node-island-child-identity/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1867, "san": 3988 }
diff --git a/tests/harness/corpus-costs/9885-node-island-child-outlives-reference/main.ts.json b/tests/harness/corpus-costs/9885-node-island-child-outlives-reference/main.ts.json
new file mode 100644
index 00000000000..d07502ec77d
--- /dev/null
+++ b/tests/harness/corpus-costs/9885-node-island-child-outlives-reference/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 3013, "san": 4671 }
diff --git a/tests/harness/corpus-costs/9886-node-island-child-stdin/main.ts.json b/tests/harness/corpus-costs/9886-node-island-child-stdin/main.ts.json
new file mode 100644
index 00000000000..2473ad8cd43
--- /dev/null
+++ b/tests/harness/corpus-costs/9886-node-island-child-stdin/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 4342, "san": 6129 }
diff --git a/tests/harness/corpus-costs/9887-node-island-child-unhandled-error/main.ts.json b/tests/harness/corpus-costs/9887-node-island-child-unhandled-error/main.ts.json
new file mode 100644
index 00000000000..4fca9934c1c
--- /dev/null
+++ b/tests/harness/corpus-costs/9887-node-island-child-unhandled-error/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1621, "san": 2957 }
diff --git a/tests/harness/corpus-costs/9888-node-island-child-general-streams/main.ts.json b/tests/harness/corpus-costs/9888-node-island-child-general-streams/main.ts.json
new file mode 100644
index 00000000000..eabdeb89f81
--- /dev/null
+++ b/tests/harness/corpus-costs/9888-node-island-child-general-streams/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 2629, "san": 35882 }
diff --git a/tests/harness/corpus-costs/9889-node-island-child-ipc/main.ts.json b/tests/harness/corpus-costs/9889-node-island-child-ipc/main.ts.json
new file mode 100644
index 00000000000..3aff6410c20
--- /dev/null
+++ b/tests/harness/corpus-costs/9889-node-island-child-ipc/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 2152, "san": 4135 }
diff --git a/tests/harness/corpus-costs/9890-node-island-child-off-during-emit/main.ts.json b/tests/harness/corpus-costs/9890-node-island-child-off-during-emit/main.ts.json
new file mode 100644
index 00000000000..e7d3ceecada
--- /dev/null
+++ b/tests/harness/corpus-costs/9890-node-island-child-off-during-emit/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 2415, "san": 2490 }
diff --git a/tests/harness/corpus-costs/9894-node-island-process-cwd/main.ts.json b/tests/harness/corpus-costs/9894-node-island-process-cwd/main.ts.json
new file mode 100644
index 00000000000..5ccc88e0d3a
--- /dev/null
+++ b/tests/harness/corpus-costs/9894-node-island-process-cwd/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 880, "san": 1276 }
diff --git a/tests/harness/corpus-costs/9895-node-island-child-close-rest-listener/main.ts.json b/tests/harness/corpus-costs/9895-node-island-child-close-rest-listener/main.ts.json
new file mode 100644
index 00000000000..57fa3d3d248
--- /dev/null
+++ b/tests/harness/corpus-costs/9895-node-island-child-close-rest-listener/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1550, "san": 2097 }
diff --git a/tests/harness/corpus-costs/9896-node-island-child-stream-pause/main.ts.json b/tests/harness/corpus-costs/9896-node-island-child-stream-pause/main.ts.json
new file mode 100644
index 00000000000..9f434de232b
--- /dev/null
+++ b/tests/harness/corpus-costs/9896-node-island-child-stream-pause/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1964, "san": 2364 }
diff --git a/tests/harness/corpus-costs/node-island-timers-clear-across/main.mjs.json b/tests/harness/corpus-costs/node-island-timers-clear-across/main.mjs.json
new file mode 100644
index 00000000000..578a9659102
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-clear-across/main.mjs.json
@@ -0,0 +1 @@
+{ "plain": 3300, "san": 4000 }
diff --git a/tests/harness/corpus-costs/node-island-timers-cross-kind-clears/main.mjs.json b/tests/harness/corpus-costs/node-island-timers-cross-kind-clears/main.mjs.json
new file mode 100644
index 00000000000..a5356ad6e8d
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-cross-kind-clears/main.mjs.json
@@ -0,0 +1 @@
+{ "plain": 2800, "san": 7800 }
diff --git a/tests/harness/corpus-costs/node-island-timers-island-clears/main.mjs.json b/tests/harness/corpus-costs/node-island-timers-island-clears/main.mjs.json
new file mode 100644
index 00000000000..a5356ad6e8d
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-island-clears/main.mjs.json
@@ -0,0 +1 @@
+{ "plain": 2800, "san": 7800 }
diff --git a/tests/harness/corpus-costs/node-island-timers-methods/main.mjs.json b/tests/harness/corpus-costs/node-island-timers-methods/main.mjs.json
new file mode 100644
index 00000000000..b2ef672d7f3
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-methods/main.mjs.json
@@ -0,0 +1 @@
+{ "plain": 1900, "san": 3100 }
diff --git a/tests/harness/corpus-costs/node-island-timers-native-clears/main.mjs.json b/tests/harness/corpus-costs/node-island-timers-native-clears/main.mjs.json
new file mode 100644
index 00000000000..c0aef3bb720
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-native-clears/main.mjs.json
@@ -0,0 +1 @@
+{ "plain": 2700, "san": 3900 }
diff --git a/tests/harness/corpus-costs/node-island-timers-promises/main.mjs.json b/tests/harness/corpus-costs/node-island-timers-promises/main.mjs.json
new file mode 100644
index 00000000000..5407ff6d6a3
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-promises/main.mjs.json
@@ -0,0 +1 @@
+{ "plain": 2800, "san": 5600 }
diff --git a/tests/harness/corpus-costs/node-island-timers-typed-package/main.ts.json b/tests/harness/corpus-costs/node-island-timers-typed-package/main.ts.json
new file mode 100644
index 00000000000..77e57a9f225
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-typed-package/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 2900, "san": 3700 }
diff --git a/tests/harness/corpus-costs/node-island-timers-typescript/main.ts.json b/tests/harness/corpus-costs/node-island-timers-typescript/main.ts.json
new file mode 100644
index 00000000000..b2ef672d7f3
--- /dev/null
+++ b/tests/harness/corpus-costs/node-island-timers-typescript/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1900, "san": 3100 }
diff --git a/tests/harness/corpus-costs/timers-cross-kind-clears.ts.json b/tests/harness/corpus-costs/timers-cross-kind-clears.ts.json
new file mode 100644
index 00000000000..a537f9ede4d
--- /dev/null
+++ b/tests/harness/corpus-costs/timers-cross-kind-clears.ts.json
@@ -0,0 +1 @@
+{ "plain": 2400, "san": 6500 }
diff --git a/tests/harness/island-conformance/known-failures.json b/tests/harness/island-conformance/known-failures.json
index 4aeacd3654b..bf537800487 100644
--- a/tests/harness/island-conformance/known-failures.json
+++ b/tests/harness/island-conformance/known-failures.json
@@ -357,11 +357,5 @@
"category": "exit",
"signature": "exit 1, Node 0: Uncaught TypeError: scriptc: this native object cannot cross into a Node island yet (scope-B NI-5 exports native values by reference) [SC8010]"
}
- },
- "packages/process-cwd": {
- "island:process-reader": {
- "category": "stdout",
- "signature": "false false"
- }
}
}
diff --git a/tests/harness/node-island-gc.test.ts b/tests/harness/node-island-gc.test.ts
index 9a9e50571e9..44f165bb86e 100644
--- a/tests/harness/node-island-gc.test.ts
+++ b/tests/harness/node-island-gc.test.ts
@@ -224,6 +224,51 @@ describe.skipIf(skip)("Node island cross-heap cycles", () => {
expect(large.totals["held exports"]).toBeLessThan(70_000);
}, 600_000);
+ test("child processes an island started and typed code used are collected with their listeners", async () => {
+ // Each child is adopted into a ChildProcess slot and its listeners
+ // capture it (tests/fixtures/node-island/gc-children.ts): the island's
+ // ChildProcess, the listener functions and the native child of every
+ // child that closed are freed, so what stays alive does not depend on
+ // how many came and went.
+ const dir = join(outDir, "gc-children");
+ mkdirSync(dir, { recursive: true });
+ const result = await compile(join(fixtures, "gc-children.ts"), {
+ outPath: join(dir, "gc-children"),
+ outDir: dir,
+ sanitize,
+ });
+ if (!result.ok)
+ throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
+ const [few, many] = sanitize ? [200, 2000] : [400, 6000];
+ const small = await run([String(few)], {}, result.binaryPath);
+ const large = await run([String(many)], {}, result.binaryPath);
+ const expected = (n: number): string => {
+ let bytes = 0;
+ let codes = 0;
+ for (let i = 0; i < n; i++) {
+ bytes += String(i).length + 1;
+ codes += 2 * (i % 3);
+ }
+ return `closed ${n} bytes ${bytes} codes ${codes} kept 0\n`;
+ };
+ expect(small.stdout).toBe(expected(few));
+ expect(large.stdout).toBe(expected(many));
+ for (const r of [small, large]) {
+ expect(r.totals["analyzed"]).toBe(r.totals["major"]);
+ // The program ends with full collections. Each child made four
+ // exports (its three listeners and the function that releases them
+ // when the child closes) and one import; what is left is what the
+ // program keeps and at most the last batches of children, whatever
+ // the number of children (measured: 2 exports after 400 and after
+ // 6,000 children, 24,000 freed).
+ expect(r.totals["held exports"]).toBeLessThan(200);
+ expect(r.totals["representatives"]).toBeLessThan(200);
+ expect(r.totals["held handles"]).toBeLessThan(50);
+ expect(r.totals["import entries"]).toBeLessThan(200);
+ }
+ expect(large.totals["freed exports"]).toBeGreaterThan(4 * many - 200);
+ }, 900_000);
+
test("a native object the program keeps keeps its identity in island weak maps", async () => {
// A typed object crosses in a capsule the export table alone holds;
// the program holds the object itself.