Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8477bfb
Clear timers across the Node island boundary
cramforce Nov 7, 2026
e2bda67
Keep the island-values fact on the lowerer, and widen the timer progr…
cramforce Nov 7, 2026
4fe12d8
Merge remote-tracking branch 'origin/main' into fix/island-timers-across
cramforce Nov 7, 2026
c2ef18a
Merge main
ctate Nov 7, 2026
72a2a5e
Address review feedback
ctate Nov 7, 2026
d73faa3
Regenerate the island host header, and bring the island conformance l…
cramforce Nov 7, 2026
340be80
Merge main into fix/island-timers-across
cramforce Nov 7, 2026
90c1d15
Bring the island conformance list up to main 50bcfe1b3
cramforce Nov 7, 2026
35d9bfe
Hold a child process a Node island made in a native ChildProcess slot
cramforce Nov 7, 2026
911643f
Record the baselines and the limitation of an island's child process …
cramforce Nov 7, 2026
4d86038
Merge remote-tracking branch 'origin/main' into fix/island-child-adop…
cramforce Nov 7, 2026
cd8ce2c
Build the island child code into the child unit instead of an object …
cramforce Nov 7, 2026
1f9f0a7
Merge main into fix/island-timers-across
cramforce Nov 7, 2026
29668ec
State what differs from Node for an island's child process, from the …
cramforce Nov 7, 2026
563fbc1
Merge remote-tracking branch 'origin/main' into fix/island-child-adop…
cramforce Nov 7, 2026
b812e65
Link the island child unit only into programs with islands and child_…
cramforce Nov 7, 2026
b470aa5
Read the islandChild link feature where the source build takes its op…
cramforce Nov 7, 2026
71f3219
Pin the plateau by forced collections, and keep the general-streams p…
cramforce Nov 7, 2026
2c42cd4
Merge remote-tracking branch 'origin/main' into fix/island-child-adop…
cramforce Nov 7, 2026
a711d84
Merge remote-tracking branch 'origin/main' into fix/island-child-adop…
cramforce Nov 7, 2026
6ca36d7
Merge fix/island-timers-across (#3182) into train/islands-5
cramforce Nov 8, 2026
f77e2a0
Merge fix/island-child-adoption (#3348) into train/islands-5
cramforce Nov 8, 2026
27f0d06
Make the merged island branches build on main
cramforce Nov 8, 2026
c408bae
Call a listener of an island's child process that an earlier listener…
cramforce Nov 8, 2026
f1bc873
Share the working directory with Node islands
cramforce Nov 8, 2026
3d48ab6
Merge main (6642a17b5f) into train/islands-5
cramforce Nov 8, 2026
90063fc
Record the CI cost of the two new island programs
cramforce Nov 8, 2026
88b072e
Merge main (59950fefd3) into train/islands-5
cramforce Nov 8, 2026
a3419c9
Run an untyped close listener of an island's child process, and pause…
cramforce Nov 8, 2026
e78aad6
Merge main (5295319e16) into train/islands-5
cramforce Nov 8, 2026
f628514
Record the CI cost of the two island child-process programs
cramforce Nov 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/limitations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ title: "Limitations"

<include>../limitations/runtime-differences-from-node-js/island-packages-that-read-the-machine-when-they-load.mdx</include>

<include>../limitations/runtime-differences-from-node-js/timers-across-native-code-and-islands.mdx</include>

<include>../limitations/runtime-differences-from-node-js/abort-listeners-across-native-code-and-islands.mdx</include>

<include>../limitations/runtime-differences-from-node-js/compile-cache-in-node-islands.mdx</include>
Expand All @@ -202,6 +204,8 @@ title: "Limitations"

<include>../limitations/runtime-differences-from-node-js/objects-converted-from-node-islands.mdx</include>

<include>../limitations/runtime-differences-from-node-js/child-processes-from-node-islands.mdx</include>

### Array bounds and runtime errors

<include>../limitations/array-bounds-and-runtime-errors.mdx</include>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions packages/compiler/src/backend/executable-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } : {}),
Expand Down
25 changes: 22 additions & 3 deletions packages/compiler/src/backend/llvm/dyn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
accessorSlotProp,
DYN,
DYN_HANDLE_KINDS,
ISLAND_ADOPTED_KINDS,
isDirentShape,
KEY_ORDER_FIELD,
shapeKeyOrders,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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})`);
Expand All @@ -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");
Expand Down Expand Up @@ -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)`);
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
Expand All @@ -1250,6 +1251,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
Expand Down
67 changes: 66 additions & 1 deletion packages/compiler/src/backend/llvm/runtime-registries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <errno\.h>/)) : 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([]);
}
});
4 changes: 4 additions & 0 deletions packages/compiler/src/backend/native-link-info-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/backend/native/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/backend/native/executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/runtime-pack-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
handleProps: boolean;
childLifecycle: boolean;
processFeatures: boolean;
islandChild: boolean;
childOptionExt: boolean;
spawnCaptureBytes: boolean;
childErrorFields: boolean;
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/executable/executable-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));

Expand All @@ -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",
]);
});

Expand All @@ -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",
]);
});
11 changes: 8 additions & 3 deletions packages/compiler/src/frontend/lowering/island-process-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,10 +18,14 @@ const EXIT_CODE_CALLS: ReadonlyMap<IrLibFn, IrLibFn> = new Map<IrLibFn, IrLibFn>
["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 = {
Expand Down
Loading
Loading