Defer evaluating Node island modules until the program uses them (NI-7)

Node islands stack, part 4 of 11 (scriptc-perf/results/islands-prs/PLAN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-22 18:49:53 -07:00
co-authored by Claude Opus 5.5
parent 106eccd4fd
commit 2f8e8b2b92
40 changed files with 2186 additions and 4 deletions
+1
View File
@@ -245,6 +245,7 @@
"packages/compiler/src/frontend/npm-static-rewrite.ts",
"packages/compiler/src/frontend/npm-static-bundled-cjs.ts",
"packages/compiler/src/library/semantic-source.ts",
"packages/compiler/src/node-islands/effect-gate.ts",
],
"rules": {
"no-restricted-imports": [
@@ -955,6 +955,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"nodeIsland.callSync": "scr_node_island_call_sync",
"nodeIsland.call": "scr_node_island_call",
"nodeIsland.require": "scr_ni_require",
"nodeIsland.defer": "scr_node_island_defer",
"nodeIsland.import": "scr_ni_import",
"nodeIsland.get": "scr_node_island_get",
"nodeIsland.getKey": "scr_ni_get",
@@ -1818,6 +1819,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"nodeIsland.call",
"nodeIsland.callSync",
"nodeIsland.require",
"nodeIsland.defer",
"nodeIsland.import",
"nodeIsland.get",
"nodeIsland.getKey",
@@ -339,6 +339,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
"scr_node_island.c",
"scr_node_island_handle.c",
"scr_node_island_export.c",
"scr_node_island_boot.c",
),
];
@@ -297,6 +297,23 @@ function loadIslandModule(path: string, loc: SrcLoc): IrExpr {
};
}
/** A static import's load: `nodeIsland.defer(path, snapshot)`, which the
* build decides after lowering (node-islands/snapshot.ts, scope-B NI-7):
* a module proved effect-free at load keeps it with its interface snapshot
* and evaluates on first use; any other becomes nodeIsland.require. */
function deferIslandModule(path: string, loc: SrcLoc): IrExpr {
return {
kind: "libCall",
fn: "nodeIsland.defer",
args: [
{ kind: "strLit", value: path, type: STRING, loc },
{ kind: "strLit", value: "", type: STRING, loc },
],
type: DYN,
loc,
};
}
/** A static import (or re-export) of an island package: loads the module
* in the importer's %init prelude. Returns false when `spec` is not an
* island module. */
@@ -324,7 +341,7 @@ export function collectIslandImport(
actions.push({
kind: "assign",
localId: binding.global.id,
value: loadIslandModule(path, loc),
value: deferIslandModule(path, loc),
loc,
});
// CommonJS named imports: Node's namespace snapshots them once the
+11 -1
View File
@@ -117,7 +117,8 @@ import {
nodeIslandNodeDiag,
type ScrDiagnostic,
} from "./diagnostics/diagnostic.js";
import { libnodeCacheKey, libnodeRecord } from "./node-islands/libnode.js";
import { libnodeCacheKey, libnodeProbePack, libnodeRecord } from "./node-islands/libnode.js";
import { finalizeDeferredIslands } from "./node-islands/snapshot.js";
import { loadLibraryProfile, type LibraryProfile } from "./library/library-profile.js";
import { clearFenceEvalCaches, decorateLibraryRefusals } from "./library/fence-eval.js";
import {
@@ -800,8 +801,17 @@ async function prepareExecutableInput(
if (typeof recorded === "string")
return { ok: false, diagnostics: [nodeIslandNodeDiag(recorded, entryPath)], sourceTexts };
mod.nodeIsland = recorded;
// Deferred island modules (scope-B NI-7): probed in the same pack.
finalizeDeferredIslands(
mod,
entryPath,
libnodeProbePack(opts.libnode, process.env, dirname(entryPath)),
);
// `beside` packaging carries the island code too (scope-B NI-4).
if (opts.libnode === "beside") placeIslandsBeside(mod, opts.outPath);
} else {
// IR, LLVM and object outputs have no pack to probe with: eager.
finalizeDeferredIslands(mod, entryPath, null);
}
}
if (warningSink !== undefined && prepared.warnings.length > 0) {
@@ -12,6 +12,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"nodeIsland.callSync",
// Island module evaluation and member operations run JavaScript.
"nodeIsland.require",
"nodeIsland.defer",
"nodeIsland.get",
"nodeIsland.getKey",
"nodeIsland.set",
@@ -190,6 +190,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* writes and calls on island values, argument lists as one dyn array.
* requireModule(specifier) of @scriptc/node lowers to require too. */
"nodeIsland.require": { argTypes: [STRING], result: DYN },
/** NI-7: a deferred island module (path, boot guards + interface snapshot). */
"nodeIsland.defer": { argTypes: [STRING, STRING], result: DYN },
"nodeIsland.import": { argTypes: [STRING], result: { kind: "promise", inner: DYN } },
"nodeIsland.get": { argTypes: [DYN, STRING], result: DYN },
"nodeIsland.getKey": { argTypes: [DYN, DYN], result: DYN },
@@ -0,0 +1,86 @@
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { effectGate } from "./effect-gate.js";
function pkg(files: Record<string, string>, type?: "module"): string {
const dir = mkdtempSync(join(tmpdir(), "effect-gate-"));
writeFileSync(join(dir, "package.json"), JSON.stringify(type ? { type } : {}));
for (const [name, text] of Object.entries(files)) {
mkdirSync(join(dir, name, ".."), { recursive: true });
writeFileSync(join(dir, name), text);
}
return join(dir, "index.js");
}
describe("effectGate", () => {
test("admits declarations, pure initializers, exports and relative requires", () => {
const entry = pkg({
"index.js": `"use strict";
const path = require("path");
const { join } = require("node:path");
const lib = require("./lib");
const VERSION = "1.0.0", LIMIT = -1;
const KEY = Symbol.for("k");
const TABLE = Object.freeze({ a: 1, [KEY]: true, re: /x+/g });
class Base {}
class Shape extends Base { static count = 0; area() { return 1; } }
function format(v) { console.log(v); return String(v); }
format.default = format;
Shape.prototype.kind = "shape";
Object.defineProperty(exports, "__esModule", { value: true });
Object.defineProperty(exports, "lib", { enumerable: true, get: function () { return lib.x; } });
exports.VERSION = VERSION;
module.exports.Shape = Shape;
exports.sep = path.sep;
exports.join = join;
exports.cache = new Map();
exports.mode = typeof window === "undefined" ? "node" : "browser";
`,
"lib.js": `exports.x = 1;\n`,
});
const r = effectGate(entry);
expect(r.ok).toBe(true);
if (r.ok) expect(r.files).toHaveLength(2);
});
test("admits ES modules with relative imports", () => {
const entry = pkg(
{
"index.js": `import { x } from "./lib.js";\nexport const ANSWER = 42;\nexport function f() { return x; }\nexport default class {}\nexport * from "./lib.js";\n`,
"lib.js": `export const x = 1;\n`,
},
"module",
);
expect(effectGate(entry).ok).toBe(true);
});
test.each([
["a top-level call", `console.log("hi");`],
["a process read", `const debug = process.env.DEBUG;`],
["an IIFE", `(function () {})();`],
["a call of the package's own function", `function f() {} const x = f();`],
["a listener", `process.on("exit", () => {});`],
["a global write", `globalThis.x = 1;`],
["a builtin write", `require("fs").readFileSync = null;`],
["a conditional", `if (true) { exports.x = 1; }`],
["an impure builtin", `require("node:sqlite");`],
["an unknown global", `const w = window;`],
["a static block", `class A { static { console.log(1); } }`],
["a getter with effects", `Object.defineProperty(exports, "x", { get() { return 1; } });`],
["a template substitution", "const s = `${{}}`;"],
])("refuses %s", (_what, text) => {
expect(effectGate(pkg({ "index.js": text })).ok).toBe(false);
});
test("refuses when a required file is effectful", () => {
const entry = pkg({
"index.js": `module.exports = require("./lib");`,
"lib.js": `console.log(1);`,
});
const r = effectGate(entry);
expect(r.ok).toBe(false);
if (!r.ok) expect(r.file.endsWith("lib.js")).toBe(true);
});
});
@@ -0,0 +1,621 @@
/* The static effect gate for deferred island modules (scope-B NI-7,
* DESIGN.md §2.7; ND §17.1). An island module may evaluate later than its
* import position only when its evaluation is unobservable: no output, no
* I/O, no listeners or timers, no writes to globals, builtins or other
* modules, no reads of inputs native code can change (process.env, cwd,
* files), and no throw. The gate is the conservative half of that proof:
* it admits a module only when the top-level code of every file it loads
* (the closure over literal require()/import specifiers) is built from the
* forms below, which cannot have effects whatever the environment. The
* build-time probe (snapshot.ts) then runs the admitted modules in libnode
* and confirms they load without throwing and without the effects it can
* observe.
*
* Admitted top-level statements (per file, CommonJS or ES module):
* - directives ("use strict"), empty statements;
* - function declarations (bodies do not run at load);
* - class declarations without decorators or static blocks, whose
* `extends`, computed member names and static field initializers are
* pure expressions;
* - var/let/const declarations with pure initializers, including object
* and array binding patterns without defaults;
* - assignments of pure expressions to `exports.x`, `module.exports`,
* `module.exports.x`, or a member chain rooted at a binding declared in
* the same file (a function, class or pure object: `F.prototype.m = …`);
* - `Object.defineProperty(target, "<name>", { … })` on such a target,
* with pure descriptor values and getters of the form
* `function () { return <pure>; }`;
* - import declarations of admitted files and allowlisted builtins;
* export declarations of the above; `export default <pure>`;
* `export … from` of admitted files.
*
* Pure expressions: literals (also regular expressions and templates with
* pure parts), identifiers bound in the file, CommonJS's `require`,
* `module`, `exports`, `__filename` and `__dirname`, and a set of global
* constructors and namespaces (never `process`); function, arrow and
* admitted class expressions; object and array literals of pure parts
* (no spreads, no accessors); `require("<literal>")` of an admitted file or
* an allowlisted builtin; member reads on required modules, allowlisted
* globals and same-file bindings; `typeof`, `void`, `!`; `===`, `!==`,
* `&&`, `||`, `??` and the conditional operator over pure operands;
* arithmetic between literals; `Symbol(…)`, `Symbol.for(…)`,
* `Object.freeze(…)` and `Object.create(…)` over pure arguments; `new` of
* Map, Set, WeakMap and WeakSet without arguments.
*
* Everything else (calls of the package's own functions, IIFEs, `this`,
* conditionals, loops, `try`, TypeScript's and Babel's interop helpers)
* keeps the module eager. Only strings cross this TypeScript 5 parser
* island. */
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { createRequire, isBuiltin } from "node:module";
import { dirname, resolve } from "node:path";
import type ts from "typescript5";
const require = createRequire(import.meta.url);
/** Builtins whose loading has no effect (no warnings, no state). */
const PURE_BUILTINS = new Set([
"assert",
"assert/strict",
"async_hooks",
"buffer",
"child_process",
"constants",
"crypto",
"diagnostics_channel",
"dns",
"events",
"fs",
"fs/promises",
"http",
"https",
"module",
"net",
"os",
"path",
"path/posix",
"path/win32",
"perf_hooks",
"querystring",
"readline",
"stream",
"stream/promises",
"string_decoder",
"timers",
"tty",
"url",
"util",
"util/types",
"v8",
"vm",
"worker_threads",
"zlib",
]);
/** Globals a pure expression may name and read members of. */
const PURE_GLOBALS = new Set([
"undefined",
"NaN",
"Infinity",
"Object",
"Function",
"Array",
"Number",
"String",
"Boolean",
"Symbol",
"BigInt",
"Math",
"JSON",
"Reflect",
"Date",
"RegExp",
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
"Map",
"Set",
"WeakMap",
"WeakSet",
"WeakRef",
"Promise",
"ArrayBuffer",
"DataView",
"Uint8Array",
"Int8Array",
"Uint16Array",
"Int16Array",
"Uint32Array",
"Int32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
"Uint8ClampedArray",
"Intl",
"TextEncoder",
"TextDecoder",
"URL",
"URLSearchParams",
"Buffer",
"isNaN",
"isFinite",
"parseInt",
"parseFloat",
"encodeURIComponent",
"decodeURIComponent",
]);
const CJS_GLOBALS = new Set(["require", "module", "exports", "__filename", "__dirname"]);
export type EffectGateResult =
| { ok: true; files: string[] }
| { ok: false; reason: string; file: string };
function isEsm(file: string): boolean {
if (file.endsWith(".mjs")) return true;
if (file.endsWith(".cjs")) return false;
for (let dir = dirname(file); ;) {
try {
const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8")) as {
type?: unknown;
};
return pkg.type === "module";
} catch {
/* none here */
}
const parent = dirname(dir);
if (parent === dir) return false;
dir = parent;
}
}
class Refusal extends Error {}
/** The static effect gate over the module at `entry` and every file it
* loads. */
export function effectGate(entry: string): EffectGateResult {
const parser = require("typescript5") as typeof ts;
const done = new Set<string>();
const queue = [entry];
while (queue.length > 0) {
const file = queue.shift()!;
if (done.has(file)) continue;
done.add(file);
try {
checkFile(parser, file, (next) => {
if (!done.has(next)) queue.push(next);
});
} catch (e) {
if (e instanceof Refusal) return { ok: false, reason: e.message, file };
throw e;
}
}
return { ok: true, files: [...done] };
}
function checkFile(parser: typeof ts, file: string, load: (file: string) => void): void {
const text = readFileSync(file, "utf8");
if (file.endsWith(".json")) return;
const esm = isEsm(file);
const sf = parser.createSourceFile(
file,
text,
parser.ScriptTarget.Latest,
true,
parser.ScriptKind.JS,
);
const SK = parser.SyntaxKind;
const refuse = (node: ts.Node, what: string): never => {
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
throw new Refusal(`${what} at line ${line + 1}`);
};
// Top-level bindings and what they hold.
const locals = new Map<string, "fn" | "class" | "module" | "value">();
for (const stmt of sf.statements) {
if (parser.isFunctionDeclaration(stmt) && stmt.name) locals.set(stmt.name.text, "fn");
else if (parser.isClassDeclaration(stmt) && stmt.name) locals.set(stmt.name.text, "class");
else if (parser.isVariableStatement(stmt)) {
for (const d of stmt.declarationList.declarations) {
const kind =
d.initializer !== undefined && isRequireish(d.initializer) ? "module" : "value";
bindNames(d.name, kind);
}
} else if (parser.isImportDeclaration(stmt) && stmt.importClause) {
const c = stmt.importClause;
if (c.name) locals.set(c.name.text, "module");
const nb = c.namedBindings;
if (nb && parser.isNamespaceImport(nb)) locals.set(nb.name.text, "module");
else if (nb) for (const el of nb.elements) locals.set(el.name.text, "module");
}
}
function bindNames(name: ts.BindingName, kind: "module" | "value"): void {
if (parser.isIdentifier(name)) locals.set(name.text, kind);
else
for (const el of name.elements) if (!parser.isOmittedExpression(el)) bindNames(el.name, kind);
}
function isRequireish(e: ts.Expression): boolean {
while (parser.isPropertyAccessExpression(e) || parser.isParenthesizedExpression(e))
e = e.expression;
return (
parser.isCallExpression(e) &&
parser.isIdentifier(e.expression) &&
e.expression.text === "require"
);
}
const resolveSpec = (spec: string, node: ts.Node, kind: "require" | "import"): void => {
if (isBuiltin(spec)) {
const bare = spec.startsWith("node:") ? spec.slice(5) : spec;
if (!PURE_BUILTINS.has(bare)) refuse(node, `a load of the builtin '${spec}'`);
return;
}
let target: string;
if (kind === "import") {
if (!spec.startsWith(".") && !spec.startsWith("/"))
refuse(node, `an import of the package '${spec}' from an ES module`);
target = resolve(dirname(file), spec);
if (!existsSync(target)) refuse(node, `an import of '${spec}', which does not exist`);
} else {
try {
target = createRequire(file).resolve(spec);
} catch {
return refuse(node, `a require of '${spec}', which does not resolve`);
}
}
try {
target = realpathSync(target);
} catch {
/* keep */
}
if (!/\.(?:c?js|mjs|json)$/.test(target)) refuse(node, `a load of '${spec}' (not JavaScript)`);
load(target);
};
const knownIdent = (name: string): boolean =>
locals.has(name) || PURE_GLOBALS.has(name) || (!esm && CJS_GLOBALS.has(name));
/** A member-read receiver whose reads cannot run unknown code. */
const readable = (e: ts.Expression): boolean => {
if (parser.isParenthesizedExpression(e)) return readable(e.expression);
if (parser.isIdentifier(e)) {
const kind = locals.get(e.text);
if (kind !== undefined) return true;
return PURE_GLOBALS.has(e.text) || (!esm && (e.text === "module" || e.text === "exports"));
}
if (parser.isPropertyAccessExpression(e)) return readable(e.expression);
if (parser.isCallExpression(e)) return isRequireCall(e);
return false;
};
const isRequireCall = (e: ts.CallExpression): boolean =>
!esm &&
parser.isIdentifier(e.expression) &&
e.expression.text === "require" &&
!locals.has("require") &&
e.arguments.length === 1 &&
parser.isStringLiteralLike(e.arguments[0]!);
const pure = (e: ts.Expression): void => {
switch (e.kind) {
case SK.NumericLiteral:
case SK.BigIntLiteral:
case SK.StringLiteral:
case SK.NoSubstitutionTemplateLiteral:
case SK.RegularExpressionLiteral:
case SK.TrueKeyword:
case SK.FalseKeyword:
case SK.NullKeyword:
return;
case SK.Identifier:
if (!knownIdent((e as ts.Identifier).text))
refuse(e, `a read of the global '${(e as ts.Identifier).text}'`);
return;
case SK.ParenthesizedExpression:
return pure((e as ts.ParenthesizedExpression).expression);
case SK.FunctionExpression:
case SK.ArrowFunction:
return;
case SK.ClassExpression:
return checkClass(e as ts.ClassExpression);
case SK.TemplateExpression:
for (const span of (e as ts.TemplateExpression).templateSpans) {
// A substitution converts its value to a string: literals only.
if (!isLiteral(span.expression)) refuse(span.expression, "a template substitution");
}
return;
case SK.ObjectLiteralExpression:
for (const p of (e as ts.ObjectLiteralExpression).properties) {
if (parser.isPropertyAssignment(p)) {
if (parser.isComputedPropertyName(p.name)) pure(p.name.expression);
pure(p.initializer);
} else if (parser.isShorthandPropertyAssignment(p)) {
pure(p.name);
} else if (parser.isMethodDeclaration(p)) {
if (parser.isComputedPropertyName(p.name)) pure(p.name.expression);
} else {
refuse(p, "a spread or accessor in an object literal");
}
}
return;
case SK.ArrayLiteralExpression:
for (const el of (e as ts.ArrayLiteralExpression).elements) {
if (parser.isSpreadElement(el)) refuse(el, "a spread in an array literal");
if (!parser.isOmittedExpression(el)) pure(el);
}
return;
case SK.PropertyAccessExpression: {
const pa = e as ts.PropertyAccessExpression;
if (pa.questionDotToken === undefined && readable(pa.expression)) {
if (parser.isCallExpression(pa.expression)) pure(pa.expression);
return;
}
return refuse(e, "a member read");
}
case SK.TypeOfExpression: {
const operand = (e as ts.TypeOfExpression).expression;
// typeof of an undeclared name does not throw.
if (parser.isIdentifier(operand)) return;
return pure(operand);
}
case SK.VoidExpression:
return pure((e as ts.VoidExpression).expression);
case SK.PrefixUnaryExpression: {
const u = e as ts.PrefixUnaryExpression;
if (u.operator === SK.ExclamationToken) return pure(u.operand);
if (isLiteral(u.operand)) return;
return refuse(e, "a unary operator on a non-literal");
}
case SK.BinaryExpression: {
const b = e as ts.BinaryExpression;
const op = b.operatorToken.kind;
if (
op === SK.EqualsEqualsEqualsToken ||
op === SK.ExclamationEqualsEqualsToken ||
op === SK.AmpersandAmpersandToken ||
op === SK.BarBarToken ||
op === SK.QuestionQuestionToken ||
op === SK.CommaToken
) {
pure(b.left);
pure(b.right);
return;
}
if (isLiteral(b.left) && isLiteral(b.right) && op !== SK.EqualsToken) return;
return refuse(e, "a binary operator on non-literals");
}
case SK.ConditionalExpression: {
const c = e as ts.ConditionalExpression;
pure(c.condition);
pure(c.whenTrue);
pure(c.whenFalse);
return;
}
case SK.CallExpression: {
const call = e as ts.CallExpression;
if (isRequireCall(call)) {
resolveSpec((call.arguments[0] as ts.StringLiteral).text, call, "require");
return;
}
const callee = call.expression;
if (
call.questionDotToken === undefined &&
parser.isPropertyAccessExpression(callee) &&
parser.isIdentifier(callee.expression) &&
!locals.has(callee.expression.text)
) {
const name = `${callee.expression.text}.${callee.name.text}`;
if (name === "Symbol.for" || name === "Object.freeze" || name === "Object.create") {
for (const a of call.arguments) pure(a);
return;
}
}
if (parser.isIdentifier(callee) && callee.text === "Symbol" && !locals.has("Symbol")) {
for (const a of call.arguments) pure(a);
return;
}
return refuse(e, "a call");
}
case SK.NewExpression: {
const n = e as ts.NewExpression;
if (
parser.isIdentifier(n.expression) &&
["Map", "Set", "WeakMap", "WeakSet"].includes(n.expression.text) &&
!locals.has(n.expression.text) &&
(n.arguments === undefined || n.arguments.length === 0)
)
return;
return refuse(e, "a `new` expression");
}
default:
return refuse(e, `a ${parser.SyntaxKind[e.kind]} expression`);
}
};
const isLiteral = (e: ts.Expression): boolean =>
e.kind === SK.NumericLiteral ||
e.kind === SK.StringLiteral ||
e.kind === SK.NoSubstitutionTemplateLiteral ||
e.kind === SK.TrueKeyword ||
e.kind === SK.FalseKeyword ||
e.kind === SK.NullKeyword ||
(parser.isPrefixUnaryExpression(e) && e.operator === SK.MinusToken && isLiteral(e.operand)) ||
(parser.isParenthesizedExpression(e) && isLiteral(e.expression));
function checkClass(c: ts.ClassLikeDeclaration): void {
const decorated = parser.canHaveDecorators(c) && (parser.getDecorators(c)?.length ?? 0) > 0;
if (decorated) refuse(c, "a decorated class");
for (const h of c.heritageClauses ?? []) for (const t of h.types) pure(t.expression);
for (const m of c.members) {
if (parser.isClassStaticBlockDeclaration(m)) refuse(m, "a class static block");
if (parser.canHaveDecorators(m) && (parser.getDecorators(m)?.length ?? 0) > 0)
refuse(m, "a decorated class member");
if (m.name !== undefined && parser.isComputedPropertyName(m.name)) pure(m.name.expression);
const isStatic =
parser.canHaveModifiers(m) &&
(parser.getModifiers(m) ?? []).some((x) => x.kind === SK.StaticKeyword);
if (isStatic && parser.isPropertyDeclaration(m) && m.initializer !== undefined)
pure(m.initializer);
}
}
/** The root binding of an assignment target, when the write is allowed. */
const writable = (target: ts.Expression): boolean => {
if (!parser.isPropertyAccessExpression(target) && !parser.isElementAccessExpression(target))
return false;
if (parser.isElementAccessExpression(target)) {
if (!isLiteral(target.argumentExpression)) return false;
}
let root: ts.Expression = target.expression;
// module.exports / module.exports.x
if (!esm && parser.isIdentifier(root) && root.text === "module" && !locals.has("module")) {
return parser.isPropertyAccessExpression(target) && target.name.text === "exports";
}
while (parser.isPropertyAccessExpression(root)) root = root.expression;
if (!parser.isIdentifier(root)) return false;
if (!esm && (root.text === "exports" || root.text === "module") && !locals.has(root.text)) {
if (root.text === "module") {
// module.exports.x…
let e: ts.Expression = target.expression;
while (
parser.isPropertyAccessExpression(e) &&
parser.isPropertyAccessExpression(e.expression)
)
e = e.expression;
return parser.isPropertyAccessExpression(e) && e.name.text === "exports";
}
return true;
}
const kind = locals.get(root.text);
return kind === "fn" || kind === "class" || kind === "value";
};
const definePropertyTarget = (e: ts.Expression): boolean => {
if (parser.isIdentifier(e)) {
if (!esm && e.text === "exports" && !locals.has("exports")) return true;
const kind = locals.get(e.text);
return kind === "fn" || kind === "class" || kind === "value";
}
return (
!esm &&
parser.isPropertyAccessExpression(e) &&
parser.isIdentifier(e.expression) &&
e.expression.text === "module" &&
e.name.text === "exports"
);
};
const checkDescriptor = (d: ts.Expression): void => {
if (!parser.isObjectLiteralExpression(d)) return refuse(d, "a computed property descriptor");
for (const p of d.properties) {
if (!parser.isPropertyAssignment(p) || !parser.isIdentifier(p.name))
return refuse(p, "an unusual property descriptor");
const key = p.name.text;
if (key === "get") {
const f = p.initializer;
const body =
parser.isFunctionExpression(f) || parser.isArrowFunction(f) ? f.body : undefined;
if (body === undefined) return refuse(p, "a getter that is not a function literal");
if (parser.isBlock(body)) {
const only = body.statements[0];
if (
body.statements.length !== 1 ||
only === undefined ||
!parser.isReturnStatement(only) ||
only.expression === undefined
)
return refuse(p, "a getter with more than `return <expression>`");
pure(only.expression);
} else {
pure(body);
}
} else if (
key === "value" ||
key === "enumerable" ||
key === "configurable" ||
key === "writable"
) {
pure(p.initializer);
} else {
return refuse(p, `a '${key}' property descriptor`);
}
}
};
const checkStatement = (stmt: ts.Statement): void => {
if (parser.isEmptyStatement(stmt) || parser.isFunctionDeclaration(stmt)) return;
if (parser.isClassDeclaration(stmt)) return checkClass(stmt);
if (parser.isVariableStatement(stmt)) {
for (const d of stmt.declarationList.declarations) {
if (!parser.isIdentifier(d.name)) {
// Destructuring reads members: only of required modules, without
// defaults.
if (d.initializer === undefined || !isRequireish(d.initializer))
refuse(d, "destructuring of a value other than a required module");
const noDefaults = (n: ts.BindingName): boolean =>
parser.isIdentifier(n) ||
n.elements.every(
(el) =>
parser.isOmittedExpression(el) ||
(el.initializer === undefined &&
el.dotDotDotToken === undefined &&
noDefaults(el.name)),
);
if (!noDefaults(d.name)) refuse(d, "a destructuring default or rest element");
}
if (d.initializer !== undefined) pure(d.initializer);
}
return;
}
if (parser.isExpressionStatement(stmt)) {
const e = stmt.expression;
if (parser.isStringLiteral(e)) return; // directive
if (parser.isBinaryExpression(e) && e.operatorToken.kind === SK.EqualsToken) {
if (!writable(e.left)) return refuse(e, "an assignment outside the module's own objects");
pure(e.right);
return;
}
if (parser.isCallExpression(e)) {
const callee = e.expression;
if (
parser.isPropertyAccessExpression(callee) &&
parser.isIdentifier(callee.expression) &&
callee.expression.text === "Object" &&
!locals.has("Object") &&
callee.name.text === "defineProperty" &&
e.arguments.length === 3 &&
definePropertyTarget(e.arguments[0]!) &&
parser.isStringLiteralLike(e.arguments[1]!)
) {
checkDescriptor(e.arguments[2]!);
return;
}
if (isRequireCall(e)) {
pure(e);
return;
}
}
return refuse(stmt, "a top-level expression with possible effects");
}
if (parser.isImportDeclaration(stmt)) {
if (stmt.attributes !== undefined) refuse(stmt, "an import with attributes");
resolveSpec((stmt.moduleSpecifier as ts.StringLiteral).text, stmt, "import");
return;
}
if (parser.isExportDeclaration(stmt)) {
if (stmt.moduleSpecifier !== undefined)
resolveSpec((stmt.moduleSpecifier as ts.StringLiteral).text, stmt, "import");
return;
}
if (parser.isExportAssignment(stmt)) return pure(stmt.expression);
return refuse(stmt, `a top-level ${parser.SyntaxKind[stmt.kind]}`);
};
for (const stmt of sf.statements) checkStatement(stmt);
}
@@ -145,3 +145,21 @@ export function libnodeCacheKey(
}
return `${option ?? ""}|${source}|${manifest}`;
}
/** The libnode pack directory the build-time probe of deferred islands
* runs in (scope-B NI-7): the same pack libnodeRecord records (before a
* `beside` copy), or null when the build host has none to run. */
export function libnodeProbePack(
option: string | undefined,
env: NodeJS.ProcessEnv = process.env,
projectDir: string = process.cwd(),
): string | null {
const fromEnv = env["SCRIPTC_LIBNODE"];
const explicit = option !== undefined && option !== "beside" ? option : fromEnv;
const source =
explicit !== undefined && explicit !== ""
? explicit
: findInstalledLibnodePack({ targetName: hostLibnodeTarget(), projectDir });
if (source === undefined || source === null || source === "" || !isAbsolute(source)) return null;
return typeof readManifest(source) === "string" ? null : resolve(source);
}
@@ -13,7 +13,7 @@ import type { IrModule } from "../ir/ir.js";
import { ORIGIN } from "./libnode.js";
import { packageDirOf } from "./origin.js";
const LOAD_FNS = new Set(["nodeIsland.require", "nodeIsland.import"]);
const LOAD_FNS = new Set(["nodeIsland.require", "nodeIsland.defer", "nodeIsland.import"]);
/** Every island module path the IR loads (the string literal argument of
* each nodeIsland.require / nodeIsland.import). */
@@ -0,0 +1,437 @@
/* Deferred island modules: the decision, the build-time probe and the
* interface snapshot (scope-B NI-7, DESIGN.md §2.7, §5.4).
*
* Lowering emits `nodeIsland.defer(path, "")` for every static import of an
* island module (lower-node-island.ts). After lowering, once the build
* knows the libnode pack the executable will load, finalizeDeferredIslands
* decides each one:
*
* - the package's `evaluate` setting (`scriptc.packages.<name>`):
* "eager" keeps the module eager; "lazy" skips the static effect gate
* and the probe's effect log (the user judged the effects benign);
* "auto" (the default) needs both;
* - the static effect gate (effect-gate.ts) over the module's closure;
* - the probe: one run of the build-time probe host
* (runtime src/tools/scr_node_island_probe.c) in a libnode environment
* from the same pack, which loads every candidate module, records what
* it can observe of their evaluation (output, warnings, listeners,
* timers, new globals, a throw) and takes the interface snapshot of
* each module's value;
*
* and rewrites the call: a deferred module keeps `nodeIsland.defer` with
* its snapshot text (format: runtime scr_node_island_boot.c), any other
* becomes the eager `nodeIsland.require(path)`. Without a pack on the
* build host there is no probe, and every island stays eager. */
import { execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readFileSync,
renameSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import type { IrExpr, IrModule } from "../ir/ir.js";
import { readProjectConfig } from "../project-config.js";
import { effectGate } from "./effect-gate.js";
import { packageDirOf } from "./origin.js";
type DeferCall = Extract<IrExpr, { kind: "libCall" }>;
function deferCalls(mod: IrModule): DeferCall[] {
const found: DeferCall[] = [];
const seen = new Set<object>();
const walk = (v: unknown): void => {
if (v === null || typeof v !== "object" || seen.has(v)) return;
seen.add(v);
if (Array.isArray(v)) {
for (const x of v) walk(x);
return;
}
const o = v as Record<string, unknown>;
if (o["kind"] === "libCall" && o["fn"] === "nodeIsland.defer") found.push(o as DeferCall);
for (const k in o) if (k !== "loc" && k !== "type") walk(o[k]);
};
walk(mod.functions);
walk(mod.globals);
return found;
}
function pathOf(call: DeferCall): string {
const arg = call.args[0];
return arg !== undefined && arg.kind === "strLit" ? arg.value : "";
}
function makeEager(call: DeferCall): void {
(call as { fn: string }).fn = "nodeIsland.require";
call.args.length = 1;
}
export interface DeferralDecision {
path: string;
deferred: boolean;
reason: string;
}
/** SCRIPTC_NODE_ISLANDS_TRACE=defer at build time prints each decision. */
function traceDecision(d: DeferralDecision): void {
const t = process.env["SCRIPTC_NODE_ISLANDS_TRACE"];
if (t !== undefined && t.includes("defer"))
process.stderr.write(
`[node-islands] ${d.deferred ? "defer" : "eager"} ${d.path}${d.reason ? `: ${d.reason}` : ""}\n`,
);
}
export function finalizeDeferredIslands(
mod: IrModule,
entryPath: string,
packDir: string | null,
): DeferralDecision[] {
const calls = deferCalls(mod);
if (calls.length === 0) return [];
const decisions: DeferralDecision[] = [];
const decide = (call: DeferCall, deferred: boolean, reason: string): void => {
const d = { path: pathOf(call), deferred, reason };
decisions.push(d);
traceDecision(d);
if (!deferred) makeEager(call);
};
if (packDir === null) {
for (const c of calls) decide(c, false, "no libnode pack on the build host to probe with");
return decisions;
}
const config = readProjectConfig(entryPath);
const candidates: { call: DeferCall; path: string; lazy: boolean; files: string[] }[] = [];
for (const call of calls) {
const path = pathOf(call);
const pkg = packageDirOf(path);
const evaluate = pkg === null ? "auto" : (config.packages.get(pkg.name)?.evaluate ?? "auto");
if (evaluate === "eager") {
decide(call, false, `"evaluate": "eager"`);
continue;
}
const gate = effectGate(path);
if (!gate.ok && evaluate !== "lazy") {
decide(call, false, `${gate.reason} in ${gate.file}`);
continue;
}
candidates.push({
call,
path,
lazy: evaluate === "lazy",
files: gate.ok ? gate.files : [path],
});
}
if (candidates.length === 0) return decisions;
const probe = runProbe(packDir, [...new Set(candidates.map((c) => c.path))]);
if (typeof probe === "string") {
for (const c of candidates) decide(c.call, false, probe);
return decisions;
}
const packGuards = packGuardLines(packDir);
for (const c of candidates) {
const result = probe.results.find((r) => r.path === c.path);
if (result?.throws !== undefined) {
decide(c.call, false, `its evaluation throws: ${result.throws}`);
continue;
}
if (result === undefined || result.root === undefined) {
decide(c.call, false, result?.reason ?? "the probe did not load it");
continue;
}
if (result.effects.length > 0 && !c.lazy) {
decide(c.call, false, `the probe saw ${result.effects.join(", ")}`);
continue;
}
const text = encodeSnapshot(probe, result.root, [...packGuards, ...fileGuardLines(c.files)]);
const arg = c.call.args[1];
if (arg !== undefined && arg.kind === "strLit") (arg as { value: string }).value = text;
decide(c.call, true, "");
}
return decisions;
}
/* ── the snapshot text (scr_node_island_boot.c) ───────────────────────── */
function lstr(s: string): string {
return `${Buffer.byteLength(s, "utf8")}:${s}`;
}
function guardLine(name: string, file: string): string | null {
try {
const st = statSync(file, { bigint: true });
return `G ${lstr(name)} ${st.size} ${st.mtimeNs}`;
} catch {
return null;
}
}
function packGuardLines(packDir: string): string[] {
const lines: string[] = [];
let manifest: { libnode?: string; bridge?: string; files?: Record<string, unknown> } = {};
try {
manifest = JSON.parse(readFileSync(join(packDir, "manifest.json"), "utf8"));
} catch {
/* the pack was validated by the build already */
}
const names = new Set<string>(Object.keys(manifest.files ?? {}));
if (manifest.libnode) names.add(manifest.libnode);
if (manifest.bridge) names.add(manifest.bridge);
for (const name of [...names].sort()) {
const line = guardLine(`@${name}`, join(packDir, name));
if (line !== null) lines.push(line);
}
return lines;
}
function fileGuardLines(files: string[]): string[] {
const lines: string[] = [];
for (const f of files) {
const line = guardLine(f, f);
if (line !== null) lines.push(line);
}
return lines;
}
type ProbeValue =
| { o: number }
| { u: 1 }
| { l: 1 }
| { b: boolean }
| { n: string }
| { s: string }
| { x: 1 };
interface ProbeObject {
f: number;
p: boolean;
m: [string, ProbeValue][];
}
interface ProbeOutput {
results: { path: string; root?: number; effects: string[]; throws?: string; reason?: string }[];
objects: Record<string, ProbeObject>;
}
function encodeValue(v: ProbeValue): string | null {
if ("o" in v) return `o${v.o}`;
if ("u" in v) return "u";
if ("l" in v) return "l";
if ("b" in v) return v.b ? "t" : "f";
if ("n" in v) return `n${v.n}`;
if ("s" in v) return `s${lstr(v.s)}`;
return null;
}
function encodeSnapshot(probe: ProbeOutput, root: number, guards: string[]): string {
const lines = [...guards];
const reached: number[] = [];
const seen = new Set<number>();
const visit = (oid: number): void => {
if (seen.has(oid) || probe.objects[oid] === undefined) return;
seen.add(oid);
reached.push(oid);
for (const [, v] of probe.objects[oid]!.m) if ("o" in v) visit(v.o);
};
visit(root);
for (const oid of reached) {
const o = probe.objects[oid]!;
lines.push(`O ${oid} ${o.f} ${o.p ? 1 : 0}`);
}
for (const oid of reached) {
for (const [key, v] of probe.objects[oid]!.m) {
const enc = encodeValue(v);
if (enc !== null) lines.push(`K ${oid} ${lstr(key)} ${enc}`);
}
}
lines.push(`R ${root}`);
return lines.join("\n");
}
/* ── the probe ────────────────────────────────────────────────────────── */
/** The bootstrap the probe host starts Node with: the dispatcher the
* bridge requires, and the probe as its `call`. */
const PROBE_BOOTSTRAP = String.raw`
"use strict";
const types = require("node:util").types;
const { createRequire } = require("node:module");
const noop = () => {};
globalThis.__scriptcNodeIsland = {
call(id, sync, file, specifier, name, args) {
try {
return "R" + JSON.stringify(probe(JSON.parse(args)));
} catch (e) {
return "T" + JSON.stringify(String((e && e.stack) || e));
}
},
setTimer: noop, clearTimer: noop, refTimer: noop, refreshTimer: noop,
setImmediate: noop, clearImmediate: noop, refImmediate: noop,
nextTick: noop, emit: noop, load: noop,
};
const fnToString = Function.prototype.toString;
const hasOwn = Object.prototype.hasOwnProperty;
function isConstructor(f) {
try { Reflect.construct(String, [], f); return true; } catch { return false; }
}
function plainClass(f) {
let source = "";
try { source = fnToString.call(f); } catch { return false; }
if (/\[native code\]\s*\}$/.test(source)) return false;
for (let o = f; o !== null && o !== Function.prototype; o = Object.getPrototypeOf(o))
if (hasOwn.call(o, Symbol.hasInstance)) return false;
return true;
}
function probe(input) {
const objects = {};
const oids = new Map();
let next = 1;
const value = (v, depth) => {
if (v === undefined) return { u: 1 };
if (v === null) return { l: 1 };
switch (typeof v) {
case "boolean": return { b: v };
case "number": return { n: Object.is(v, -0) ? "-0" : String(v) };
case "string": return v.isWellFormed() ? { s: v } : { x: 1 };
case "symbol": return object(v, 2, depth);
case "function":
if (types.isProxy(v)) return { x: 1 };
return object(v, 1 | (isConstructor(v) ? 32 : 0), depth);
case "object":
if (types.isProxy(v) || types.isPromise(v) || types.isNativeError(v) ||
ArrayBuffer.isView(v) || types.isExternal(v)) return { x: 1 };
return object(v, Array.isArray(v) ? 8 : 0, depth);
default: return { x: 1 };
}
};
const object = (v, flags, depth) => {
if (oids.has(v)) return { o: oids.get(v) };
const id = next++;
oids.set(v, id);
const entry = { f: flags, p: (flags & 1) !== 0 && plainClass(v), m: [] };
objects[id] = entry;
if (typeof v === "symbol") return { o: id };
// The module's value and its direct members: every own data property
// (namespace member lists); functions anywhere: name and length.
const keys = depth === 0 || (depth === 1 && typeof v !== "function")
? Object.getOwnPropertyNames(v)
: typeof v === "function" ? ["name", "length"] : [];
for (const key of keys) {
let d;
try { d = Object.getOwnPropertyDescriptor(v, key); } catch { continue; }
if (d === undefined || !("value" in d)) continue;
entry.m.push([key, value(d.value, depth + 1)]);
}
return { o: id };
};
const effects = [];
const watch = (owner, key, label) => {
const original = owner[key];
if (typeof original !== "function") return () => {};
owner[key] = function (...a) { effects.push(label); return original.apply(this, a); };
return () => { owner[key] = original; };
};
const results = [];
for (const path of input.modules) {
effects.length = 0;
const globals = new Set(Reflect.ownKeys(globalThis));
const restore = [
watch(process.stdout, "write", "stdout output"),
watch(process.stderr, "write", "stderr output"),
watch(process, "emitWarning", "a process warning"),
watch(process, "on", "a process listener"),
watch(process, "addListener", "a process listener"),
watch(process, "once", "a process listener"),
watch(process, "prependListener", "a process listener"),
watch(process, "nextTick", "process.nextTick"),
watch(globalThis, "setTimeout", "a timer"),
watch(globalThis, "setInterval", "a timer"),
watch(globalThis, "setImmediate", "an immediate"),
watch(globalThis, "queueMicrotask", "a microtask"),
];
let exportsValue, threw;
try { exportsValue = createRequire(path)(path); } catch (e) { threw = String((e && e.message) || e); }
for (const r of restore) r();
for (const k of Reflect.ownKeys(globalThis))
if (!globals.has(k)) effects.push("a new global " + String(k));
if (threw !== undefined) { results.push({ path, effects: [...effects], throws: threw }); continue; }
const root = value(exportsValue, 0);
if (!("o" in root)) {
results.push({ path, effects: [...effects], reason: "its value is not an object" });
continue;
}
results.push({ path, root: root.o, effects: [...new Set(effects)] });
}
return { results, objects };
}
`;
function runtimeSrc(): string {
const require = createRequire(import.meta.url);
return join(dirname(require.resolve("@scriptc/runtime/package.json")), "src");
}
/** The probe host executable, built once per probe source and bridge
* header (cached under the temporary directory). */
function probeHost(): string | string[] {
const src = runtimeSrc();
const source = join(src, "tools", "scr_node_island_probe.c");
const header = join(src, "scr_node_bridge.h");
const hash = createHash("sha256")
.update(readFileSync(source))
.update(readFileSync(header))
.update(PROBE_BOOTSTRAP)
.digest("hex")
.slice(0, 16);
const dir = join(tmpdir(), `scriptc-node-probe-${process.getuid?.() ?? 0}`, hash);
const exe = join(dir, "scr-node-probe");
const bootstrap = join(dir, "bootstrap.js");
if (existsSync(exe) && existsSync(bootstrap)) return [exe, bootstrap];
mkdirSync(dir, { recursive: true });
const tmp = `${exe}.${process.pid}`;
const errors: string[] = [];
for (const cc of [process.env["CC"], "clang", "cc"]) {
if (cc === undefined || cc === "") continue;
const r = spawnSync(cc, ["-O1", "-I", src, source, "-o", tmp, "-ldl"], { encoding: "utf8" });
if (r.status === 0) {
writeFileSync(`${bootstrap}.${process.pid}`, PROBE_BOOTSTRAP);
renameSync(`${bootstrap}.${process.pid}`, bootstrap);
renameSync(tmp, exe);
return [exe, bootstrap];
}
errors.push(`${cc}: ${r.error?.message ?? r.stderr.trim().split("\n")[0]}`);
}
return `cannot build the probe host (${errors.join("; ")})`;
}
function runProbe(packDir: string, modules: string[]): ProbeOutput | string {
const host = probeHost();
if (typeof host === "string") return host;
const [exe, bootstrap] = host as [string, string];
const input = join(dirname(exe), `input.${process.pid}.${Date.now()}.json`);
writeFileSync(input, JSON.stringify({ modules }));
let out: string;
try {
out = execFileSync(exe, [packDir, bootstrap, input], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 64 * 1024 * 1024,
timeout: 60_000,
});
} catch (e) {
const err = e as { stderr?: string; message: string };
return `the probe failed: ${(err.stderr ?? "").trim().split("\n")[0] || err.message}`;
}
try {
rmSync(input);
} catch {
/* best effort */
}
if (!out.startsWith("R")) return `the probe threw: ${out.slice(1, 400)}`;
return JSON.parse(out.slice(1)) as ProbeOutput;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6840-node-island-deferred-unused/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6841-node-island-deferred-call/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6842-node-island-effectful-eager/main.ts"
],
"diags": []
}
@@ -168,6 +168,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_node_island.c", "nodeIsland"],
["scr_node_island_handle.c", "nodeIsland"],
["scr_node_island_export.c", "nodeIsland"],
["scr_node_island_boot.c", "nodeIsland"],
];
export function variantsFor(source) {
+12
View File
@@ -545,11 +545,18 @@ static ScrNodeBridge ni_hosted;
/* ── loading Node ─────────────────────────────────────────────────────── */
void (*scr_ni_started_hook)(void) = NULL;
static void ni_start(void) {
if (ni_bridge != NULL) return;
/* Timers pending now keep their deadlines relative to this moment: the
* time Node takes to load is invisible to their order. */
double since = scr_now_ms();
const char *trace = getenv("SCRIPTC_NODE_ISLANDS_TRACE");
if (trace != NULL && strstr(trace, "boot") != NULL) {
fflush(stdout);
fprintf(stderr, "[node-islands] starting Node\n");
}
const char *dir = ni_pack_dir();
char path[4352];
snprintf(path, sizeof path, "%s/%s", dir, NI_BRIDGE_FILE);
@@ -608,6 +615,9 @@ static void ni_start(void) {
ni_hosted = *bridge;
ni_hosted.run_loop = &ni_run_loop;
scr_loop_host(&ni_hosted, since);
/* NI-7: deferred island modules evaluate now, before the operation that
* needed Node, in their import order. */
if (scr_ni_started_hook != NULL) scr_ni_started_hook();
}
const ScrNodeBridge *scr_ni_bridge(void) {
@@ -617,6 +627,8 @@ const ScrNodeBridge *scr_ni_bridge(void) {
const ScrNodeBridge *scr_ni_bridge_loaded(void) { return ni_bridge; }
const char *scr_ni_pack_dir(void) { return ni_pack_dir(); }
void scr_ni_on_node_stack(void (*job)(void *), void *arg) { ni_on_node_stack(job, arg); }
/* ── calls ────────────────────────────────────────────────────────────── */
+24
View File
@@ -123,6 +123,30 @@ void scr_ni_exports_release_all(void); /* at exit, after Node stopped */
/* Promise settlement callback (SCR_NODE_ISLANDS build of scr_async.c). */
void scr_promise_on_settle_sync(ScrPromise *in, void (*cb)(ScrPromise *dst, ScrPromise *src));
/* ── deferred island modules (scope-B NI-7, scr_node_island_boot.c) ─────
* nodeIsland.defer(path, snapshot): a statically imported island module the
* build proved effect-free at load evaluates on first real use instead of
* at its import position. The snapshot (lower-node-island.ts) holds the
* boot guards (the libnode pack's and the module files' identity) and the
* interface snapshot (export names, kinds, function name/length, primitive
* constants). Guards pass: the module's value is a stub handle and Node
* does not start; a guard fails: the module loads here, eagerly. When Node
* starts for any reason, every deferred module evaluates first, in import
* order, and each stub is reconciled with its real value (same ref, so
* identity holds for the rest of the program). +1, NULL with an exception
* pending (an eager load that threw). */
ScrDyn *scr_node_island_defer(ScrStr *path, ScrStr *snapshot);
/* Between the boot and handle units. */
ScrDyn *scr_ni_stub_new(uint32_t flags, void *stub); /* +1 box over a fresh stub ref */
void *scr_ni_stub_of(const ScrDyn *value); /* the stub entry of an unmaterialized stub, or NULL */
bool scr_ni_stub_reconcile(ScrDyn *stub, ScrDyn *real);
ScrDyn *scr_ni_boot_member(void *stub, const char *key, size_t len); /* +1 or NULL: unknown */
bool scr_ni_boot_plain_class(void *stub);
const char *scr_ni_pack_dir(void); /* the libnode pack directory this run uses */
void scr_ni_boot_stub_failed(void *stub); /* raises the module's load error */
/* Called at the end of Node's start (scr_node_island.c), set by the boot unit. */
extern void (*scr_ni_started_hook)(void);
/* Island operations the compiler emits (lib-shared.ts adapters). */
ScrDyn *scr_node_island_apply(ScrDyn *fn, ScrDyn *args);
ScrDyn *scr_node_island_construct(ScrDyn *ctor, ScrDyn *args);
+468
View File
@@ -0,0 +1,468 @@
/* Node islands: deferred evaluation (scope-B milestone NI-7). Linked with
* the other island units into programs whose IR reaches Node.
*
* A statically imported island module the build admitted for deferral
* (frontend/lowering/lower-node-island.ts: the static effect gate plus the
* build-time probe in libnode) does not load at its import position. Its
* %init calls scr_node_island_defer(path, snapshot) instead, which:
*
* 1. checks the boot guards without loading Node: the identity (size and
* mtime) of the libnode pack's bridge and library this run would load,
* and of every file of the module's closure. A snapshot taken with one
* libnode or one install of the package never applies to another: any
* mismatch loads the module here, eagerly, at its position;
* 2. otherwise builds stubs from the interface snapshot: one island handle
* (a ScrNodeRef with no bridge id, scr_node_island_handle.c) per object
* the probe saw, deduplicated by the probe's object identity, so two
* exports of the same function are one stub. Before Node starts, the
* snapshot answers `typeof`, `name`/`length` of functions, primitive
* constants and the members it recorded, and `instanceof` against an
* ordinary class (false for every native value: no island object can
* exist yet).
*
* Every other use needs Node. Starting it (for any reason) runs the hook
* below before the operation that needed it: every deferred module
* evaluates, in import order, and each stub is reconciled with its real
* value: the stub's ref takes over the value's bridge id, so the program's
* stub boxes and every later crossing of the value are the same handle and
* `===` holds (identity reconciliation). A module that throws when it
* finally loads raises its error at each use of its stubs.
*
* Snapshot format (one record per line; <len>:<bytes> strings):
* G <len>:<path> <size> <mtime-ns> guard file ("@name": in the pack)
* O <oid> <flags> <plain-class> an object (SCR_NVF_* flags)
* K <oid> <len>:<key> <value> a data member of object <oid>
* R <oid> the module's value
* where <value> is o<oid>, u, l, t, f, n<number> or s<len>:<bytes>. */
#define _XOPEN_SOURCE 700
#define _DARWIN_C_SOURCE 1
#include "scr_runtime.h"
#include "scr_node_bridge.h"
#include "scr_node_island.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#if !defined(_WIN32) && !defined(__wasi__)
SCR_COLD static void nb_oom(void) { scr_trap("scriptc: out of memory\n"); }
typedef struct {
const char *key;
size_t key_len;
char kind; /* o u l t f n s */
uint32_t oid;
double num;
const char *str;
size_t str_len;
} NbMember;
struct NbModule;
typedef struct NbStub {
uint32_t oid;
uint32_t flags;
bool plain;
ScrDyn *box; /* +1 */
NbMember *members;
size_t nmembers, members_cap;
struct NbModule *module; /* the module whose value reaches it first */
uint32_t parent; /* 0: the module's value */
const char *key; /* its member name in the parent */
size_t key_len;
ScrCaught *error; /* why it could not be reconciled */
} NbStub;
typedef struct NbModule {
char *path;
char *snapshot; /* owns the strings the stubs point into */
uint32_t root;
uint32_t *order; /* stubs to reconcile after the root, parents first */
size_t norder;
ScrCaught *error; /* the module's load error */
} NbModule;
static NbStub **nb_stubs = NULL; /* by oid */
static size_t nb_stubs_cap = 0;
static NbModule **nb_modules = NULL;
static size_t nb_nmodules = 0, nb_modules_cap = 0;
static bool nb_installed = false;
static int nb_trace = -1;
static bool nb_tracing(void) {
if (nb_trace < 0) {
const char *t = getenv("SCRIPTC_NODE_ISLANDS_TRACE");
nb_trace = t != NULL && strstr(t, "boot") != NULL;
}
return nb_trace == 1;
}
static NbStub *nb_stub(uint32_t oid) { return oid < nb_stubs_cap ? nb_stubs[oid] : NULL; }
/* ── parsing ──────────────────────────────────────────────────────────── */
typedef struct {
const char *p, *end;
bool bad;
} NbCursor;
static void nb_space(NbCursor *c) {
if (c->p < c->end && *c->p == ' ') c->p++;
}
static uint64_t nb_uint(NbCursor *c) {
nb_space(c);
uint64_t v = 0;
const char *start = c->p;
while (c->p < c->end && *c->p >= '0' && *c->p <= '9') v = v * 10 + (uint64_t)(*c->p++ - '0');
if (c->p == start) c->bad = true;
return v;
}
static const char *nb_str(NbCursor *c, size_t *len) {
uint64_t n = nb_uint(c);
if (c->bad || c->p >= c->end || *c->p != ':' || (uint64_t)(c->end - c->p - 1) < n) {
c->bad = true;
*len = 0;
return "";
}
c->p++;
const char *s = c->p;
c->p += n;
*len = (size_t)n;
return s;
}
static void nb_line_end(NbCursor *c) {
while (c->p < c->end && *c->p != '\n') c->p++;
if (c->p < c->end) c->p++;
}
/* ── guards ───────────────────────────────────────────────────────────── */
typedef struct {
char *path;
bool ok;
} NbGuardMemo;
static NbGuardMemo *nb_memo = NULL;
static size_t nb_nmemo = 0, nb_memo_cap = 0;
/* Whether the file still has the recorded size and mtime. */
static bool nb_guard(const char *path, size_t len, uint64_t size, uint64_t mtime_ns) {
char full[4352];
if (len > 0 && path[0] == '@')
snprintf(full, sizeof full, "%s/%.*s", scr_ni_pack_dir(), (int)(len - 1), path + 1);
else
snprintf(full, sizeof full, "%.*s", (int)len, path);
for (size_t i = 0; i < nb_nmemo; i++)
if (strcmp(nb_memo[i].path, full) == 0) return nb_memo[i].ok;
struct stat st;
bool ok = false;
if (stat(full, &st) == 0) {
#if defined(__APPLE__)
uint64_t m = (uint64_t)st.st_mtimespec.tv_sec * 1000000000u + (uint64_t)st.st_mtimespec.tv_nsec;
#else
uint64_t m = (uint64_t)st.st_mtim.tv_sec * 1000000000u + (uint64_t)st.st_mtim.tv_nsec;
#endif
ok = (uint64_t)st.st_size == size && m == mtime_ns;
}
if (!ok && nb_tracing()) fprintf(stderr, "[node-islands] boot guard failed: %s\n", full);
if (nb_nmemo == nb_memo_cap) {
size_t cap = nb_memo_cap ? nb_memo_cap * 2 : 16;
NbGuardMemo *grown = realloc(nb_memo, cap * sizeof *grown);
if (grown == NULL) nb_oom();
nb_memo = grown;
nb_memo_cap = cap;
}
nb_memo[nb_nmemo].path = strdup(full);
if (nb_memo[nb_nmemo].path == NULL) nb_oom();
nb_memo[nb_nmemo++].ok = ok;
return ok;
}
/* ── materialization (Node started) ──────────────────────────────────── */
static ScrCaught *nb_changed(const NbModule *m) {
char message[4400];
snprintf(message, sizeof message,
"scriptc: the Node island module %s no longer matches its build-time interface snapshot",
m->path);
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
return scr_exc_take();
}
static void nb_materialize(NbModule *m) {
ScrStr *path = scr_str_new(m->path, strlen(m->path));
ScrDyn *real = scr_ni_require(path);
scr_str_release(path);
if (real == NULL) {
m->error = scr_exc_take();
return;
}
NbStub *root = nb_stub(m->root);
bool ok = scr_ni_stub_reconcile(root->box, real);
scr_dyn_release(real);
if (!ok) {
m->error = nb_changed(m);
return;
}
for (size_t i = 0; i < m->norder; i++) {
NbStub *s = nb_stub(m->order[i]);
NbStub *parent = nb_stub(s->parent);
if (scr_ni_stub_of(s->box) == NULL) continue; /* reached through an earlier module */
if (parent->error != NULL || scr_ni_stub_of(parent->box) != NULL) {
s->error = scr_caught_retain(parent->error != NULL ? parent->error : m->error);
continue;
}
ScrDyn *value = scr_ni_get_str(parent->box, s->key, s->key_len);
if (value == NULL) {
s->error = scr_exc_take();
continue;
}
if (!scr_ni_stub_reconcile(s->box, value)) s->error = nb_changed(m);
scr_dyn_release(value);
}
}
static void nb_started(void) {
if (nb_tracing() && nb_nmodules > 0)
fprintf(stderr, "[node-islands] Node started: evaluating %zu deferred module(s)\n", nb_nmodules);
for (size_t i = 0; i < nb_nmodules; i++) nb_materialize(nb_modules[i]);
}
void scr_ni_boot_stub_failed(void *stub) {
NbStub *s = stub;
ScrCaught *error = s->error != NULL ? s->error : s->module->error;
if (error != NULL) {
scr_rethrow(error);
return;
}
ScrCaught *changed = nb_changed(s->module);
s->error = changed;
scr_rethrow(changed);
}
/* ── snapshot answers before Node starts ─────────────────────────────── */
ScrDyn *scr_ni_boot_member(void *stub, const char *key, size_t len) {
NbStub *s = stub;
for (size_t i = 0; i < s->nmembers; i++) {
NbMember *m = &s->members[i];
if (m->key_len != len || memcmp(m->key, key, len) != 0) continue;
switch (m->kind) {
case 'o': {
NbStub *target = nb_stub(m->oid);
return target != NULL ? scr_dyn_retain(target->box) : NULL;
}
case 'u': return scr_dyn_retain(scr_dyn_undefined());
case 'l': return scr_dyn_new_null();
case 't': return scr_dyn_new_bool(true);
case 'f': return scr_dyn_new_bool(false);
case 'n': return scr_dyn_new_num(m->num);
case 's': {
ScrStr *str = scr_str_new(m->str, m->str_len);
ScrDyn *d = scr_dyn_new_str(str);
scr_str_release(str);
return d;
}
default: return NULL;
}
}
return NULL;
}
bool scr_ni_boot_plain_class(void *stub) { return ((NbStub *)stub)->plain; }
/* ── nodeIsland.defer ─────────────────────────────────────────────────── */
static void nb_cleanup(void) {
for (size_t i = 0; i < nb_stubs_cap; i++) {
NbStub *s = nb_stubs[i];
if (s == NULL) continue;
scr_dyn_release(s->box);
if (s->error != NULL) scr_caught_release(s->error);
free(s->members);
free(s);
}
free(nb_stubs);
nb_stubs = NULL;
nb_stubs_cap = 0;
for (size_t i = 0; i < nb_nmodules; i++) {
NbModule *m = nb_modules[i];
if (m->error != NULL) scr_caught_release(m->error);
free(m->order);
free(m->snapshot);
free(m->path);
free(m);
}
free(nb_modules);
nb_modules = NULL;
nb_nmodules = 0;
}
static void nb_install(void) {
if (nb_installed) return;
nb_installed = true;
scr_ni_handles_install(); /* stub boxes use the island handle tag */
scr_ni_started_hook = nb_started;
scr_atexit(nb_cleanup);
}
static ScrDyn *nb_eager(ScrStr *path, const char *why) {
if (nb_tracing())
fprintf(stderr, "[node-islands] evaluating %.*s eagerly: %s\n", (int)path->len, path->data, why);
return scr_ni_require(path);
}
static NbStub *nb_new_stub(uint32_t oid, uint32_t flags, bool plain, NbModule *m) {
if (oid >= nb_stubs_cap) {
size_t cap = nb_stubs_cap ? nb_stubs_cap : 64;
while (cap <= oid) cap *= 2;
NbStub **grown = realloc(nb_stubs, cap * sizeof *grown);
if (grown == NULL) nb_oom();
memset(grown + nb_stubs_cap, 0, (cap - nb_stubs_cap) * sizeof *grown);
nb_stubs = grown;
nb_stubs_cap = cap;
}
NbStub *s = calloc(1, sizeof *s);
if (s == NULL) nb_oom();
s->oid = oid;
s->flags = flags;
s->plain = plain;
s->module = m;
s->box = scr_ni_stub_new(flags, s);
nb_stubs[oid] = s;
return s;
}
ScrDyn *scr_node_island_defer(ScrStr *path, ScrStr *snapshot) {
if (scr_ni_bridge_loaded() != NULL) return nb_eager(path, "Node is already running");
nb_install();
/* Pass 1: the guards. */
NbCursor c = {snapshot->data, snapshot->data + snapshot->len, false};
while (c.p < c.end && !c.bad) {
char tag = *c.p++;
if (tag == 'G') {
size_t len;
const char *file = nb_str(&c, &len);
uint64_t size = nb_uint(&c);
uint64_t mtime = nb_uint(&c);
if (c.bad) break;
if (!nb_guard(file, len, size, mtime)) return nb_eager(path, "a boot guard failed");
}
nb_line_end(&c);
}
if (c.bad) return nb_eager(path, "malformed snapshot");
/* Pass 2: the stubs. The module owns a copy of the snapshot text, which
* the members' keys and strings point into. */
NbModule *m = calloc(1, sizeof *m);
if (m == NULL) nb_oom();
m->path = malloc(path->len + 1);
m->snapshot = malloc(snapshot->len + 1);
if (m->path == NULL || m->snapshot == NULL) nb_oom();
memcpy(m->path, path->data, path->len);
m->path[path->len] = '\0';
memcpy(m->snapshot, snapshot->data, snapshot->len);
m->snapshot[snapshot->len] = '\0';
c = (NbCursor){m->snapshot, m->snapshot + snapshot->len, false};
while (c.p < c.end && !c.bad) {
char tag = *c.p++;
if (tag == 'O') {
uint32_t oid = (uint32_t)nb_uint(&c);
uint32_t flags = (uint32_t)nb_uint(&c);
bool plain = nb_uint(&c) != 0;
if (!c.bad && nb_stub(oid) == NULL) nb_new_stub(oid, flags, plain, m);
} else if (tag == 'K') {
uint32_t oid = (uint32_t)nb_uint(&c);
NbMember mem = {0};
mem.key = nb_str(&c, &mem.key_len);
nb_space(&c);
if (c.p >= c.end) c.bad = true;
if (c.bad) break;
mem.kind = *c.p++;
if (mem.kind == 'o') {
mem.oid = (uint32_t)nb_uint(&c);
} else if (mem.kind == 'n') {
char num[64];
size_t n = 0;
while (c.p < c.end && *c.p != '\n' && n + 1 < sizeof num) num[n++] = *c.p++;
num[n] = '\0';
mem.num = strtod(num, NULL);
} else if (mem.kind == 's') {
mem.str = nb_str(&c, &mem.str_len);
}
NbStub *s = nb_stub(oid);
/* Members of objects an earlier module already described stay. */
if (!c.bad && s != NULL && s->module == m) {
if (s->nmembers == s->members_cap) {
size_t cap = s->members_cap ? s->members_cap * 2 : 4;
NbMember *grown = realloc(s->members, cap * sizeof *grown);
if (grown == NULL) nb_oom();
s->members = grown;
s->members_cap = cap;
}
s->members[s->nmembers++] = mem;
}
} else if (tag == 'R') {
m->root = (uint32_t)nb_uint(&c);
}
nb_line_end(&c);
}
NbStub *root = nb_stub(m->root);
if (c.bad || root == NULL) {
free(m->snapshot);
free(m->path);
free(m);
return nb_eager(path, "malformed snapshot");
}
/* Reconciliation order: breadth first from the module's value, so each
* stub's parent is real before the stub's own member read. */
size_t cap = 8;
m->order = malloc(cap * sizeof *m->order);
if (m->order == NULL) nb_oom();
for (size_t head = 0, at = (size_t)-1;; head++) {
NbStub *p = at == (size_t)-1 ? root : nb_stub(m->order[at]);
for (size_t i = 0; i < p->nmembers; i++) {
NbMember *mem = &p->members[i];
if (mem->kind != 'o') continue;
NbStub *child = nb_stub(mem->oid);
if (child == NULL || child == root || child->module != m || child->key != NULL) continue;
child->parent = p->oid;
child->key = mem->key;
child->key_len = mem->key_len;
if (m->norder == cap) {
cap *= 2;
uint32_t *grown = realloc(m->order, cap * sizeof *grown);
if (grown == NULL) nb_oom();
m->order = grown;
}
m->order[m->norder++] = child->oid;
}
(void)head;
at = at == (size_t)-1 ? 0 : at + 1;
if (at >= m->norder) break;
}
if (nb_nmodules == nb_modules_cap) {
size_t mcap = nb_modules_cap ? nb_modules_cap * 2 : 4;
NbModule **grown = realloc(nb_modules, mcap * sizeof *grown);
if (grown == NULL) nb_oom();
nb_modules = grown;
nb_modules_cap = mcap;
}
nb_modules[nb_nmodules++] = m;
if (nb_tracing()) fprintf(stderr, "[node-islands] deferred %s\n", m->path);
return scr_dyn_retain(root->box);
}
#else
ScrDyn *scr_node_island_defer(ScrStr *path, ScrStr *snapshot) {
(void)snapshot;
return scr_ni_require(path);
}
#endif /* !_WIN32 && !__wasi__ */
+109 -1
View File
@@ -44,6 +44,12 @@ typedef struct ScrNodeRef {
size_t rc;
uint32_t id;
uint32_t flags; /* SCR_NVF_* */
/* NI-7: a stub of a deferred island module's value (id 0) carries its
* interface-snapshot entry (scr_node_island_boot.c) until Node starts and
* the boot unit reconciles it with the real value. `alias`: the real ref
* when the value already had one (identity split, traced). */
void *stub;
struct ScrNodeRef *alias;
} ScrNodeRef;
static ScrNodeRef **ni_refs = NULL; /* by bridge id */
@@ -176,6 +182,15 @@ static void *nh_ref_retain_v(void *h) {
static void nh_ref_release(ScrNodeRef *ref) {
if (--ref->rc != 0) return;
if (ref->alias != NULL) {
nh_ref_release(ref->alias);
free(ref);
return;
}
if (ref->id == 0) { /* a stub never materialized, or a transplanted ref */
free(ref);
return;
}
if (ref->id < ni_refs_cap && ni_refs[ref->id] == ref) ni_refs[ref->id] = NULL;
if (!ni_stopped && scr_ni_bridge_loaded() != NULL) scr_ni_bridge_loaded()->drop(ref->id);
free(ref);
@@ -199,7 +214,7 @@ static ScrNodeRef *nh_ref(uint32_t id, uint32_t flags) {
ref->rc++;
return ref;
}
ref = malloc(sizeof *ref);
ref = calloc(1, sizeof *ref);
if (ref == NULL) nh_oom();
ref->rc = 1;
ref->id = id;
@@ -208,6 +223,73 @@ static ScrNodeRef *nh_ref(uint32_t id, uint32_t flags) {
return ref;
}
/* ── NI-7 stubs (deferred island modules, scr_node_island_boot.c) ─────── */
ScrDyn *scr_ni_stub_new(uint32_t flags, void *stub) {
ScrNodeRef *ref = calloc(1, sizeof *ref);
if (ref == NULL) nh_oom();
ref->rc = 1;
ref->flags = flags;
ref->stub = stub;
ScrDyn *box = scr_dyn_new_handle(ref, SCR_DYNH_NODE_ISLAND);
ref->rc--; /* the box holds the only reference */
return box;
}
void *scr_ni_stub_of(const ScrDyn *value) {
if (value == NULL || value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_NODE_ISLAND)
return NULL;
return ((ScrNodeRef *)value->v.handle.ptr)->stub;
}
/* Identity reconciliation: the stub box `stub` becomes the program's
* handle of the real island value `real` (borrowed). When the runtime saw
* the value for the first time, the stub takes over its bridge id, so every
* later crossing of the value answers the stub's ref and `===` holds; the
* fresh ref is retired without dropping the bridge entry. Answers false
* when the value is not an island object (the snapshot no longer applies). */
bool scr_ni_stub_reconcile(ScrDyn *stub, ScrDyn *real) {
ScrNodeRef *s = stub->v.handle.ptr;
if (!scr_ni_is_handle(real)) return false;
ScrNodeRef *r = real->v.handle.ptr;
if (r != s && r->stub != NULL) return false;
s->stub = NULL;
if (r == s) return true;
if (r->rc == 1 && r->alias == NULL) {
/* Only `real` holds the fresh ref: move its id to the stub. */
s->id = r->id;
s->flags = r->flags;
ni_refs[r->id] = s;
r->id = 0;
real->v.handle.ptr = s;
s->rc++;
r->rc = 0;
free(r);
return true;
}
/* The program already holds the value through another ref: the stub
* forwards to it (identity split for `===` between the two). */
s->alias = r;
r->rc++;
s->id = r->id;
s->flags = r->flags;
if (getenv("SCRIPTC_NODE_ISLANDS_TRACE") != NULL)
fprintf(stderr, "[node-islands] stub identity split (id %u)\n", r->id);
return true;
}
/* A stub whose Node value is needed: starting Node materializes every
* deferred module (scr_node_island_boot.c), after which the ref is real.
* False with the module's load error pending. */
static bool nh_live(ScrNodeRef *ref) {
if (ref->stub == NULL) return true;
void *stub = ref->stub;
scr_ni_bridge();
if (ref->stub == NULL) return true;
scr_ni_boot_stub_failed(stub);
return false;
}
static const ScrDynHandleOps nh_ops;
bool scr_ni_is_handle(const ScrDyn *value) {
@@ -557,6 +639,7 @@ static bool nh_to_node(ScrDyn *v, ScrNodeValue *out, NhKeep *keep) {
case SCR_DYN_HANDLE:
if (v->v.handle.tag == SCR_DYNH_NODE_ISLAND) {
ScrNodeRef *ref = v->v.handle.ptr;
if (!nh_live(ref)) return false;
out->tag = SCR_NV_HANDLE;
out->flags = ref->flags;
out->u.handle = ref->id;
@@ -699,6 +782,7 @@ static bool nh_receiver(ScrDyn *obj, uint32_t *id, const char *what) {
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
return false;
}
if (!nh_live(ref)) return false;
*id = ref->id;
return true;
}
@@ -741,6 +825,8 @@ ScrPromise *scr_ni_import(ScrStr *path) {
}
ScrDyn *scr_ni_get(ScrDyn *obj, ScrDyn *key) {
if (key != NULL && key->kind == SCR_DYN_STR && scr_ni_stub_of(obj) != NULL)
return scr_ni_get_str(obj, key->v.str->data, key->v.str->len);
uint32_t id;
if (!nh_receiver(obj, &id, "a property read")) return NULL;
nh_enter();
@@ -757,6 +843,12 @@ ScrDyn *scr_ni_get(ScrDyn *obj, ScrDyn *key) {
}
ScrDyn *scr_ni_get_str(ScrDyn *obj, const char *key, size_t len) {
void *stub = scr_ni_stub_of(obj);
if (stub != NULL && scr_ni_bridge_loaded() == NULL) {
/* Before Node starts, the interface snapshot answers what it holds. */
ScrDyn *known = scr_ni_boot_member(stub, key, len);
if (known != NULL) return known;
}
uint32_t id;
if (!nh_receiver(obj, &id, "a property read")) return NULL;
nh_enter();
@@ -905,6 +997,13 @@ ScrStr *scr_ni_typeof(ScrDyn *value) {
}
int scr_ni_instanceof(ScrDyn *value, ScrDyn *ctor) {
/* A stub class before Node starts: no island object exists yet, and a
* native value crossing into Node is never an instance of an ordinary
* island class (the snapshot says whether the class is one). */
void *stub = scr_ni_stub_of(ctor);
if (stub != NULL && scr_ni_bridge_loaded() == NULL && !scr_ni_is_handle(value) &&
scr_ni_boot_plain_class(stub))
return 0;
uint32_t id;
if (!nh_receiver(ctor, &id, "instanceof")) return -1;
nh_enter();
@@ -957,6 +1056,11 @@ ScrPromise *scr_ni_await(ScrDyn *value) {
ScrPromise *boxed = value != NULL ? scr_dyn_promise_of(value) : NULL;
if (boxed != NULL) return scr_promise_retain(boxed);
ScrNodeRef *ref = nh_ref_of(value);
if (ref != NULL && !nh_live(ref)) {
ScrPromise *p = scr_promise_new();
scr_promise_reject_pending(p);
return p;
}
ScrPromise *p = scr_promise_new();
if (ref == NULL) {
scr_promise_fulfill_ref(p, scr_dyn_retain(value != NULL ? value : scr_dyn_undefined()),
@@ -985,6 +1089,7 @@ static ScrDyn *nh_inspect_fn = NULL;
ScrStr *scr_ni_inspect(const ScrDyn *value, double recurse, double depth) {
ScrNodeRef *ref = nh_ref_of(value);
if (ref == NULL) return scr_str_new("", 0);
if (!nh_live(ref)) return scr_str_new("", 0);
if (nh_inspect_fn == NULL) {
ScrStr *util = scr_str_new("node:util", 9);
ScrDyn *module = nh_load(util, 0);
@@ -1123,6 +1228,9 @@ static void nh_cleanup(void) {
}
void scr_ni_handles_install(void) {
static bool installed = false;
if (installed) return;
installed = true;
scr_dyn_handle_install(SCR_DYNH_NODE_ISLAND, &nh_ops);
scr_atexit(nh_cleanup);
}
@@ -0,0 +1,183 @@
/* The build-time probe host for deferred Node islands (scope-B NI-7).
* Not a runtime unit: the compiler (node-islands/snapshot.ts) builds it
* once per bridge ABI and runs it against the libnode pack the program
* will load, so the interface snapshot comes from that exact Node.
*
* scr-node-probe <pack directory> <bootstrap.js> <input.json>
*
* It starts Node from the pack's bridge with the bootstrap (which defines
* the dispatcher and the probe), makes one synchronous call with the input
* JSON, prints the outcome ("R<json>" or "T<json>") on stdout and stops
* Node. Never includes a Node or V8 header. */
#include "scr_node_bridge.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__APPLE__)
#define BRIDGE_NAME "libscr_node_bridge.dylib"
#else
#define BRIDGE_NAME "libscr_node_bridge.so"
#endif
static char *slurp(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (f == NULL) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
char *data = malloc((size_t)n + 1);
if (data == NULL || fread(data, 1, (size_t)n, f) != (size_t)n) {
fclose(f);
free(data);
return NULL;
}
fclose(f);
data[n] = '\0';
*len = (size_t)n;
return data;
}
static void host_settle(uint32_t id, const char *outcome, size_t len) {
(void)id;
(void)outcome;
(void)len;
}
static void host_exit(int code) {
fflush(stdout);
_Exit(code == 0 ? 3 : code);
}
static void host_run(uint32_t kind, uint32_t token) {
(void)kind;
(void)token;
}
static void host_io_state(ScrNodeIoState *state) { memset(state, 0, sizeof *state); }
static void host_void(void) {}
static void host_listen(uint32_t event, const char *name, uint32_t listener, int add, int once) {
(void)event;
(void)name;
(void)listener;
(void)add;
(void)once;
}
static uintptr_t host_zero(void) { return 0; }
static void host_settle_value(uint32_t token, int rejected, const ScrNodeValue *value) {
(void)token;
(void)rejected;
(void)value;
}
static void host_bytes_free(void *owner) { (void)owner; }
static int host_export_call(uint32_t id, const ScrNodeValue *this_, const ScrNodeValue *args,
size_t argc, ScrNodeValue *out) {
(void)id;
(void)this_;
(void)args;
(void)argc;
out->tag = SCR_NV_UNDEFINED;
return 0;
}
static int host_export_construct(uint32_t id, const ScrNodeValue *args, size_t argc,
ScrNodeValue *out) {
(void)id;
(void)args;
(void)argc;
out->tag = SCR_NV_UNDEFINED;
return 0;
}
static int host_export_get(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out) {
(void)id;
(void)key;
(void)out;
return 2;
}
static int host_export_set(uint32_t id, const ScrNodeValue *key, const ScrNodeValue *value,
ScrNodeValue *out) {
(void)id;
(void)key;
(void)value;
(void)out;
return 2;
}
static int host_export_has(uint32_t id, const ScrNodeValue *key) {
(void)id;
(void)key;
return 2;
}
static int host_export_delete(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out) {
(void)id;
(void)key;
(void)out;
return 2;
}
static int host_export_keys(uint32_t id, ScrNodeValue *out) {
(void)id;
(void)out;
return 2;
}
static void host_export_id(uint32_t id) { (void)id; }
static const ScrNodeBridgeHost kHost = {
.settle = host_settle,
.exit = host_exit,
.run = host_run,
.io_state = host_io_state,
.io_dispatch = host_void,
.microtasks_done = host_void,
.listen = host_listen,
.stack_limit = host_zero,
.stack_start = host_zero,
.settle_value = host_settle_value,
.bytes_free = host_bytes_free,
.export_call = host_export_call,
.export_construct = host_export_construct,
.export_get = host_export_get,
.export_set = host_export_set,
.export_has = host_export_has,
.export_delete = host_export_delete,
.export_keys = host_export_keys,
.export_watch = host_export_id,
.export_release = host_export_id,
};
int main(int argc, char **argv) {
if (argc != 4) {
fprintf(stderr, "usage: scr-node-probe <pack directory> <bootstrap.js> <input.json>\n");
return 2;
}
char path[4096];
snprintf(path, sizeof path, "%s/%s", argv[1], BRIDGE_NAME);
void *library = dlopen(path, RTLD_NOW | RTLD_GLOBAL);
if (library == NULL) {
fprintf(stderr, "scr-node-probe: %s\n", dlerror());
return 1;
}
const ScrNodeBridge *(*entry)(void) =
(const ScrNodeBridge *(*)(void))dlsym(library, SCR_NODE_BRIDGE_ENTRY);
const ScrNodeBridge *bridge = entry != NULL ? entry() : NULL;
if (bridge == NULL || bridge->abi != SCR_NODE_BRIDGE_ABI) {
fprintf(stderr, "scr-node-probe: incompatible bridge at %s\n", path);
return 1;
}
size_t blen = 0, ilen = 0;
char *bootstrap = slurp(argv[2], &blen);
char *input = slurp(argv[3], &ilen);
if (bootstrap == NULL || input == NULL) {
fprintf(stderr, "scr-node-probe: cannot read the bootstrap or the input\n");
return 1;
}
char *error = bridge->start(&kHost, bootstrap, argv[0], 0);
if (error != NULL) {
fprintf(stderr, "scr-node-probe: %s\n", error);
return 1;
}
size_t len = 0;
char *outcome = bridge->call(0, 1, "", "", "probe", input, ilen, &len);
fwrite(outcome, 1, len, stdout);
fflush(stdout);
bridge->release(outcome);
/* No orderly stop: the probe's modules may hold handles; the process
* exits here. */
_Exit(0);
}
+1
View File
@@ -28,6 +28,7 @@ const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([
"packages/compiler/src/frontend/jsx-source.test.ts",
"packages/compiler/src/frontend/ts7/world-check.ts",
"packages/compiler/src/frontend/ts7/source-parser.test.ts",
"packages/compiler/src/node-islands/effect-gate.ts",
"packages/compiler/src/library/semantic-source.ts",
"packages/compiler/test/ts7/harness.ts",
"packages/compiler/test/ts7/resolver-parity.test.ts",
@@ -0,0 +1,24 @@
// @requires-libnode
// A statically imported Node island package the program never calls. The
// package's evaluation has no effects, so the build defers it (scope-B
// NI-7): Node never starts and libnode is never mapped, and what the
// program reads of the package before that comes from the build-time
// interface snapshot (kinds, function names and lengths, constants).
import { existsSync, readFileSync } from "node:fs";
import { format, helpers, LIMIT, Shape, VERSION } from "lazy-pkg";
function libnodeMapped(): boolean {
if (!existsSync("/proc/self/maps")) return false;
return readFileSync("/proc/self/maps", "utf8").includes("libnode");
}
console.log("start");
console.log(typeof format, format.name, format.length);
console.log(typeof Shape, Shape.name, Shape.length);
console.log(VERSION, LIMIT, typeof helpers);
const plain = { kind: "native" };
console.log("instanceof", plain instanceof Shape);
const alias = format;
console.log("identity", alias === format);
console.log("libnode mapped:", libnodeMapped());
console.log("end");
@@ -0,0 +1,11 @@
export declare const VERSION: string;
export declare const LIMIT: number;
export declare class Shape {
sides: number;
constructor(sides: number);
describe(): string;
}
export declare function format(value: unknown, width?: number): string;
export declare function getFormat(): typeof format;
export declare const helpers: { twice(n: number): number; sep: string };
export declare function counted(): number;
@@ -0,0 +1,37 @@
"use strict";
const path = require("path");
const VERSION = "1.2.3";
const LIMIT = 42;
class Shape {
constructor(sides) {
this.sides = sides;
}
describe() {
return `shape with ${this.sides} sides`;
}
}
function format(value, width) {
return String(value).padStart(width || 0, ".");
}
function getFormat() {
return format;
}
const helpers = {
twice(n) {
return n * 2;
},
sep: path.sep,
};
let calls = 0;
function counted() {
calls += 1;
return calls;
}
module.exports = { VERSION, LIMIT, Shape, format, getFormat, helpers, counted };
@@ -0,0 +1 @@
{ "name": "lazy-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-deferred-unused", "private": true, "type": "module", "scriptc": { "packages": { "lazy-pkg": "node" } } }
@@ -0,0 +1,19 @@
// @requires-libnode
// Deferred island packages (scope-B NI-7) that the program uses later:
// before the first real use, reads come from the interface snapshot; the
// first call starts Node, evaluates the deferred modules in import order
// and reconciles each stub with its real value, so values the program
// took before Node started are the same objects afterwards.
import { counted, format, getFormat, helpers, Shape, VERSION } from "lazy-pkg";
import { ANSWER, greet } from "esm-lazy";
console.log("before first use", VERSION, ANSWER, typeof greet, greet.name, greet.length);
const keepFormat = format;
const keepShape = Shape;
console.log("calls", format("a", 3), format(7, 2));
console.log("identity", getFormat() === format, keepFormat === format, getFormat() === keepFormat);
const s = new Shape(4);
console.log(s.describe(), s instanceof Shape, s instanceof keepShape, s.sides);
console.log(helpers.twice(21), helpers.sep);
console.log("counted", counted(), counted());
console.log(greet("world"), ANSWER);
@@ -0,0 +1,2 @@
export declare const ANSWER: number;
export declare function greet(who: string): string;
@@ -0,0 +1,5 @@
export const ANSWER = 42;
export function greet(who) {
return `hello, ${who}`;
}
@@ -0,0 +1 @@
{ "name": "esm-lazy", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,11 @@
export declare const VERSION: string;
export declare const LIMIT: number;
export declare class Shape {
sides: number;
constructor(sides: number);
describe(): string;
}
export declare function format(value: unknown, width?: number): string;
export declare function getFormat(): typeof format;
export declare const helpers: { twice(n: number): number; sep: string };
export declare function counted(): number;
@@ -0,0 +1,37 @@
"use strict";
const path = require("path");
const VERSION = "1.2.3";
const LIMIT = 42;
class Shape {
constructor(sides) {
this.sides = sides;
}
describe() {
return `shape with ${this.sides} sides`;
}
}
function format(value, width) {
return String(value).padStart(width || 0, ".");
}
function getFormat() {
return format;
}
const helpers = {
twice(n) {
return n * 2;
},
sep: path.sep,
};
let calls = 0;
function counted() {
calls += 1;
return calls;
}
module.exports = { VERSION, LIMIT, Shape, format, getFormat, helpers, counted };
@@ -0,0 +1 @@
{ "name": "lazy-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-deferred-call", "private": true, "type": "module", "scriptc": { "packages": { "lazy-pkg": "node", "esm-lazy": "node" } } }
@@ -0,0 +1,8 @@
// @requires-libnode
// An island package whose evaluation prints is not deferred (scope-B NI-7's
// static effect gate refuses top-level calls): it evaluates at its import
// position, so its output comes before the program's own, as in Node.
import { shout } from "loud-pkg";
console.log("main body");
console.log(shout("done"));
@@ -0,0 +1 @@
export declare function shout(s: string): string;
@@ -0,0 +1,9 @@
"use strict";
console.log("loud-pkg: evaluating");
console.error("loud-pkg: a line on stderr");
function shout(s) {
return `${s.toUpperCase()}!`;
}
module.exports = { shout };
@@ -0,0 +1 @@
{ "name": "loud-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-effectful-eager", "private": true, "type": "module", "scriptc": { "packages": { "loud-pkg": "node" } } }