mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
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:
co-authored by
Claude Opus 5.5
parent
106eccd4fd
commit
2f8e8b2b92
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6840-node-island-deferred-unused/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6841-node-island-deferred-call/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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) {
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__ */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
+11
@@ -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;
|
||||
+37
@@ -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 };
|
||||
+1
@@ -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);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare const ANSWER: number;
|
||||
export declare function greet(who: string): string;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const ANSWER = 42;
|
||||
|
||||
export function greet(who) {
|
||||
return `hello, ${who}`;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "esm-lazy", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
+11
@@ -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;
|
||||
+37
@@ -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 };
|
||||
+1
@@ -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"));
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function shout(s: string): string;
|
||||
+9
@@ -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 };
|
||||
+1
@@ -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" } } }
|
||||
Reference in New Issue
Block a user