Share modules between a program and its Node islands through loader hooks (NI-6)

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-22 18:49:53 -07:00
co-authored by Claude Opus 5.5
parent 2f8e8b2b92
commit 5fa44d9243
55 changed files with 1938 additions and 34 deletions
@@ -966,6 +966,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"nodeIsland.construct": "scr_node_island_construct",
"nodeIsland.instanceof": "scr_node_island_instanceof",
"nodeIsland.classMethods": "scr_node_island_class_methods",
"nodeIsland.share": "scr_node_island_share",
"nodeIsland.sharedTdz": "scr_node_island_shared_tdz",
"nodeIsland.classMethodsOf": "scr_node_island_class_methods_of",
"dyn.keySet": "scr_dyn_key_set",
"dyn.keySetComputed": "scr_dyn_key_set_computed",
"dyn.keyDelete": "scr_dyn_key_delete",
@@ -258,6 +258,44 @@ export function nativeSubclassOfIslandDiag(loc: SrcLoc): ScrDiagnostic {
};
}
/** SC8020 — a stateful module the program compiles is duplicated across
* the Node island boundary (an explicit "duplicate", or a form NI-6 cannot
* share yet): each side has its own state. */
export function sharedModuleDuplicatedDiag(
module: string,
why: string,
loc: SrcLoc,
): ScrDiagnostic {
return {
code: "SC8020",
message: `module '${module}' is duplicated across the Node island boundary although it is stateful: ${why}; the program and its islands each see their own copy`,
loc,
hint: 'remove "share": "duplicate" from its scriptc.packages entry to share one instance',
};
}
/** SC8022 — island code extends a class of a module the program compiles.
* A native instance cannot take an island subclass's overrides, so the
* module is not owned by the program: a package runs in Node for both
* sides (the build retries with it as a Node island); a program module is
* refused. */
export function sharedClassExtendedDiag(
pkg: string | null,
file: string,
base: string,
loc: SrcLoc,
): ScrDiagnostic {
return {
code: "SC8022",
message:
pkg !== null
? `package '${pkg}' is shared with a Node island that extends its class '${base}': it runs in Node for both sides`
: `module '${file}' cannot be shared with a Node island that extends its class '${base}' (island subclasses of native classes)`,
loc,
hint: "move the subclass into the program, or mark the module as a Node island",
};
}
/** SC8002 — Node islands are off (or limited to marked islands) and an
* npm package is outside the native subset. */
export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic {
@@ -14,6 +14,11 @@ import * as ts from "../ts7/adapter.js";
import { collectRebindableFunctions, isRebindableFunction } from "./rebindable-functions.js";
import { bindingSource } from "../binding-source.js";
import type { Lowerer } from "./lowerer.js";
import {
sharedInitFlagStores,
sharedModulePlans,
sharedModuleRegistrations,
} from "./lower-shared-modules.js";
import { dirname as dirnamePath, resolve as resolvePath } from "node:path";
import { packageNameOfPath, probeNodeImportRefusal } from "../npm.js";
import { isNpmStaticPackage } from "../npm-static.js";
@@ -2973,6 +2978,10 @@ export function lowerFileInit(
});
}
if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning());
// Modules shared with Node islands register before any module runs
// (NI-6); the plan also creates their declarations' TDZ flags.
if (sharedModulePlans(lowerer).length > 0 && sf === lowerer.entry)
header.push(...sharedModuleRegistrations(lowerer));
// Top-level `this` of a CommonJS module: its object exists before the
// module's first statement (lowerCjsTopLevelThis).
const exportsThis = lowerer.cjsExportsThis.get(sf);
@@ -3209,6 +3218,7 @@ export function lowerFileInit(
stmt,
deferred ? [deferred] : lowerer.lowerStmts([stmt]),
);
if (lowerer.sharedInitFlags.size > 0) stmtIr.push(...sharedInitFlagStores(lowerer, stmt));
// Class EXPRESSIONS inside this statement queued their static
// inits while it lowered: they land immediately before it — JS's
// order for the supported whole-initializer positions.
@@ -590,8 +590,27 @@ function withClassMethods(
className: string,
blame: ts.Node,
): IrExpr {
const builder = classMethodsBuilderOf(lowerer, className, blame, value.loc);
if (builder === null) return value;
return {
kind: "libCall",
fn: "nodeIsland.classMethods",
args: [value, builder],
type: DYN,
loc: value.loc,
};
}
/** The builder of a class's method table (a dyn closure answering
* {name: unbound method value}), or null for a class without methods. */
function classMethodsBuilderOf(
lowerer: Lowerer,
className: string,
blame: ts.Node,
loc: SrcLoc,
): IrExpr | null {
const info = lowerer.classes.get(className);
if (info === undefined || info.def.runtime) return value;
if (info === undefined || info.def.runtime) return null;
const names: string[] = [];
for (
let c: typeof info | undefined = info;
@@ -601,8 +620,7 @@ function withClassMethods(
for (const m of c.def.methods ?? [])
if (!names.includes(m) && m !== "constructor") names.push(m);
}
if (names.length === 0) return value;
const loc = value.loc;
if (names.length === 0) return null;
const builder = `%island.methods:${className}`;
if (!lowerer.classMethodValueHelpers.has(builder)) {
lowerer.classMethodValueHelpers.add(builder);
@@ -631,13 +649,75 @@ function withClassMethods(
type: funcTypeFromParamShapes([], DYN),
loc,
};
return {
kind: "libCall",
fn: "nodeIsland.classMethods",
args: [value, lowerer.coerceToExpected(closure, DYN)],
type: DYN,
return lowerer.coerceToExpected(closure, DYN);
}
/** NI-6: the method-table builder of the class a shared module exports
* (`ident` is its declaration's name), or null. */
export function classMethodsBuilder(
lowerer: Lowerer,
ident: ts.Identifier,
loc: SrcLoc,
): IrExpr | null {
const decl = ident.parent;
if (decl === undefined || !ts.isClassDeclaration(decl)) return null;
return classMethodsBuilderOf(lowerer, lowerer.classNamer(decl), ident, loc);
}
/** NI-6: `new C(...)` from island code for the class a shared module
* exports: a function with the constructor's parameters (its dyn thunk
* checks the island's arguments) that constructs an instance, as a dyn
* closure, or null when the constructor's shape is not a plain list of
* parameters (the island's `new` then throws). */
export function classConstructHelper(
lowerer: Lowerer,
ident: ts.Identifier,
loc: SrcLoc,
): IrExpr | null {
const decl = ident.parent;
if (decl === undefined || !ts.isClassDeclaration(decl)) return null;
const className = lowerer.classNamer(decl);
const info = lowerer.classes.get(className);
if (info === undefined || info.def.runtime || info.localClass || info.generic) return null;
for (let c: typeof info | null = info; c; c = c.base) if (c.def.baseValueGlobal) return null;
if (lowerer.inheritsBuiltinErrorCtor(info)) return null;
const shapes = info.ctorParams;
if (shapes.some((s) => s.mode !== "required" && s.mode !== "omittable")) return null;
const name = `%island.new:${className}`;
if (!lowerer.classMethodValueHelpers.has(name)) {
lowerer.classMethodValueHelpers.add(name);
lowerer.noteEdge(`%${className}.constructor`);
const params = shapes.map((s, i) => ({ localId: `p${i}`, name: `p${i}`, type: s.type }));
const objectType: IrType = { kind: "object", className };
lowerer.liftedFns.push({
name,
params,
returnType: objectType,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
body: [
{
kind: "return",
value: {
kind: "new",
className,
args: params.map((p) => varRef(p.localId, p.type, loc)),
type: objectType,
loc,
},
loc,
},
],
loc,
});
}
const closure: IrExpr = {
kind: "closure",
fnName: name,
captures: [],
type: funcTypeFromParamShapes(shapes, { kind: "object", className }),
loc,
};
return lowerer.coerceToExpected(closure, DYN);
}
/** Native values NI-5 exports into the island by reference: functions,
@@ -0,0 +1,529 @@
/* Shared modules between the program and its Node islands (scope-B NI-6,
* DESIGN.md §2.6). A module the program compiles that island code also
* imports would otherwise exist twice, with two states. Rule: each module
* has exactly one owner.
*
* - Program modules (user code): always shared, owned by the program.
* - npm packages compiled natively and imported by island code: shared
* when stateful (module-level state written after evaluation, exported
* classes, evaluation effects), duplicated when pure (each side has its
* own copy, which nobody can observe). `scriptc.packages.<name>.share`
* ("shared" | "duplicate") overrides; duplicating a stateful package is
* SC8020, a warning, because the two copies diverge.
*
* A shared module is registered at program start (nodeIsland.share, never
* loads Node) with its accessor: a generated function that answers the
* current values of its exports as a dyn array, the TDZ sentinel for a let
* or const whose declaration has not run yet (a flag the module's %init
* sets right after the declaration). The bootstrap's loader hooks
* (runtime scr_node_island_hooks.cjs) resolve island imports of the module
* to a generated ES module over those values.
*
* Island code is found syntactically: the files requireModule() loads and
* every JavaScript file of an island package, then the files they import
* that the program does not compile (Node's side of the closure). An
* import that resolves to a compiled module makes it a candidate. A
* specifier inside a comment or string can only add a candidate (an extra
* shared module), never lose one. */
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
import { resolveFrom } from "../../node-islands/resolve.js";
import { join } from "node:path";
import * as ts from "../ts7/adapter.js";
import { locOf, isCjsJsFile } from "../program.js";
import { PoisonError, newFnCtx, type Lowerer } from "./lowerer.js";
import { BOOL, DYN, STRING, VOID, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
import { resolveBareModule } from "../resolve.js";
import { packageDirOf } from "../../node-islands/origin.js";
import { readProjectConfig } from "../../project-config.js";
import {
sharedClassExtendedDiag,
sharedModuleDuplicatedDiag,
} from "../../diagnostics/diagnostic.js";
import { classConstructHelper, classMethodsBuilder } from "./lower-node-island.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { funcTypeFromParamShapes } from "./call-signatures.js";
type ExportKind = "function" | "class" | "let" | "const" | "var";
interface SharedExport {
name: string;
kind: ExportKind;
/** The declaration's name: its reference is the export's value. */
ident: ts.Identifier;
/** let/const: the flag global set after the declaration ran. */
flag?: string;
}
export interface SharedModulePlan {
sf: ts.SourceFile;
/** The real path Node resolves the module to. */
path: string;
exports: SharedExport[];
}
const MAX_ISLAND_FILES = 4000;
const SPECIFIERS = [
/\bfrom\s*["']([^"'\n]+)["']/g,
/\bimport\s*["']([^"'\n]+)["']/g,
/\brequire\s*\(\s*["']([^"'\n]+)["']\s*\)/g,
/\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g,
];
const REQUIRE_MODULE = /\brequireModule\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
function real(path: string): string {
try {
return realpathSync(path);
} catch {
return path;
}
}
function isJsFile(path: string): boolean {
return /\.(?:[cm]?js)$/.test(path);
}
/** The files `spec` may name from `from` (require and import conditions). */
function resolveAll(from: string, spec: string): string[] {
if (spec.startsWith("node:")) return [];
const out = new Set<string>();
try {
out.add(real(resolveFrom(from, spec)));
} catch {
/* not resolvable for require */
}
if (!spec.startsWith(".") && !spec.startsWith("/")) {
for (const kind of ["import", "require"] as const) {
const js = resolveBareModule(from, spec, "js-only", kind);
if (js !== null) out.add(real(js.typesFile));
}
}
return [...out];
}
function packageJsFiles(dir: string, out: string[]): void {
let names: string[];
try {
names = readdirSync(dir);
} catch {
return;
}
for (const name of names) {
if (out.length >= MAX_ISLAND_FILES) return;
if (name === "node_modules" || name.startsWith(".")) continue;
const path = join(dir, name);
let isDirectory: boolean;
try {
isDirectory = statSync(path).isDirectory();
} catch {
continue;
}
if (isDirectory) packageJsFiles(path, out);
else if (isJsFile(name)) out.push(real(path));
}
}
function isExported(stmt: ts.Node): boolean {
const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined;
return mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true;
}
function isDefaultExport(stmt: ts.Node): boolean {
const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined;
return mods?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) === true;
}
/** The top-level declarations of a module by name. */
function localDeclarations(
sf: ts.SourceFile,
): Map<string, { kind: ExportKind; ident: ts.Identifier }> {
const out = new Map<string, { kind: ExportKind; ident: ts.Identifier }>();
for (const stmt of sf.statements.nodes) {
if (ts.isFunctionDeclaration(stmt) && stmt.name && stmt.body)
out.set(stmt.name.text, { kind: "function", ident: stmt.name });
else if (ts.isClassDeclaration(stmt) && stmt.name)
out.set(stmt.name.text, { kind: "class", ident: stmt.name });
else if (ts.isVariableStatement(stmt)) {
const flags = stmt.declarationList.flags;
const kind: ExportKind =
(flags & ts.NodeFlags.Const) !== 0
? "const"
: (flags & ts.NodeFlags.Let) !== 0
? "let"
: "var";
for (const d of stmt.declarationList.declarations.nodes)
if (ts.isIdentifier(d.name)) out.set(d.name.text, { kind, ident: d.name });
}
}
return out;
}
/** The module's value exports (re-exports and default exports of
* expressions are not shared: NOTES.md shortcut). */
function moduleExports(sf: ts.SourceFile): SharedExport[] {
const locals = localDeclarations(sf);
const out: SharedExport[] = [];
for (const stmt of sf.statements.nodes) {
if (ts.isExportDeclaration(stmt)) {
if (stmt.moduleSpecifier || stmt.isTypeOnly || !stmt.exportClause) continue;
if (!ts.isNamedExports(stmt.exportClause)) continue;
for (const el of stmt.exportClause.elements.nodes) {
if (el.isTypeOnly) continue;
const local = locals.get((el.propertyName ?? el.name).text);
if (local !== undefined) out.push({ name: el.name.text, ...local });
}
continue;
}
if (!isExported(stmt)) continue;
const def = isDefaultExport(stmt);
if (ts.isFunctionDeclaration(stmt) && stmt.name && stmt.body)
out.push({ name: def ? "default" : stmt.name.text, kind: "function", ident: stmt.name });
else if (ts.isClassDeclaration(stmt) && stmt.name)
out.push({ name: def ? "default" : stmt.name.text, kind: "class", ident: stmt.name });
else if (ts.isVariableStatement(stmt)) {
const flags = stmt.declarationList.flags;
const kind: ExportKind =
(flags & ts.NodeFlags.Const) !== 0
? "const"
: (flags & ts.NodeFlags.Let) !== 0
? "let"
: "var";
for (const d of stmt.declarationList.declarations.nodes)
if (ts.isIdentifier(d.name)) out.push({ name: d.name.text, kind, ident: d.name });
}
}
return out;
}
/** The name of a class imported from `spec` that the island file extends
* (`class X extends Base` or `extends ns.Base`), or null. */
function extendedImport(text: string, spec: string): string | null {
const quoted = spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const names: string[] = [];
const named = new RegExp(
`import\\s*(?:(\\w+)\\s*,\\s*)?\\{([^}]*)\\}\\s*from\\s*["']${quoted}["']`,
"g",
);
for (const m of text.matchAll(named)) {
if (m[1] !== undefined) names.push(m[1]);
for (const part of m[2]!.split(",")) {
const local = part
.trim()
.split(/\s+as\s+/)
.at(-1)
?.trim();
if (local) names.push(local);
}
}
const star = new RegExp(`import\\s*\\*\\s*as\\s+(\\w+)\\s*from\\s*["']${quoted}["']`, "g");
for (const m of text.matchAll(star)) names.push(`${m[1]!}\\.\\w+`);
const def = new RegExp(`import\\s+(\\w+)\\s+from\\s*["']${quoted}["']`, "g");
for (const m of text.matchAll(def)) names.push(m[1]!);
for (const name of names) {
const m = new RegExp(`\\bclass\\s+[\\w$]*\\s*extends\\s+(${name})\\b`).exec(text);
if (m !== null) return m[1]!;
}
return null;
}
const MUTATING_METHODS = new Set([
"set",
"add",
"delete",
"clear",
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
"fill",
"copyWithin",
]);
/** Why a module is stateful (one reason), or null when it is pure: no
* module-level binding reachable from its exports is written after
* evaluation, no exported class (its instances may cross or meet
* instanceof in island code), and no top-level effect beyond declarations. */
export function statefulReason(sf: ts.SourceFile): string | null {
const moduleLevel = new Map<string, string>(); // name -> kind
for (const stmt of sf.statements.nodes) {
if (ts.isClassDeclaration(stmt) && isExported(stmt))
return `it exports the class '${stmt.name?.text ?? "default"}'`;
if (ts.isVariableStatement(stmt)) {
const flags = stmt.declarationList.flags;
for (const d of stmt.declarationList.declarations.nodes) {
if (!ts.isIdentifier(d.name)) continue;
if ((flags & ts.NodeFlags.Const) === 0) {
if (isExported(stmt)) return `it exports the mutable binding '${d.name.text}'`;
moduleLevel.set(d.name.text, "let");
} else {
moduleLevel.set(d.name.text, "const");
}
}
continue;
}
if (
ts.isFunctionDeclaration(stmt) ||
ts.isClassDeclaration(stmt) ||
ts.isImportDeclaration(stmt) ||
ts.isExportDeclaration(stmt) ||
ts.isInterfaceDeclaration(stmt) ||
ts.isTypeAliasDeclaration(stmt) ||
ts.isExportAssignment(stmt) ||
ts.isEmptyStatement(stmt)
)
continue;
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; // directives
return "its evaluation has effects beyond declarations";
}
// Writes from function bodies to module-level bindings.
let reason: string | null = null;
const visit = (node: ts.Node, inFunction: boolean): void => {
if (reason !== null) return;
const fn = ts.isFunctionLike(node);
if (inFunction) {
if (ts.isBinaryExpression(node) && ts.isAssignmentOperator(node.operatorToken.kind)) {
let target: ts.Node = node.left;
while (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target))
target = target.expression;
if (ts.isIdentifier(target) && moduleLevel.has(target.text))
reason = `its function bodies write the module-level binding '${target.text}'`;
} else if (
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
(node.operator === ts.SyntaxKind.PlusPlusToken ||
node.operator === ts.SyntaxKind.MinusMinusToken) &&
ts.isIdentifier(node.operand) &&
moduleLevel.has(node.operand.text)
) {
reason = `its function bodies write the module-level binding '${node.operand.text}'`;
} else if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.expression) &&
moduleLevel.has(node.expression.expression.text) &&
MUTATING_METHODS.has(node.expression.name.text)
) {
reason = `its function bodies mutate the module-level '${node.expression.expression.text}'`;
}
}
ts.forEachChild(node, (child) => visit(child, inFunction || fn));
};
visit(sf, false);
return reason;
}
/** The modules the program shares with its islands (computed once). */
export function sharedModulePlans(lowerer: Lowerer): SharedModulePlan[] {
if (lowerer.sharedModules !== null) return lowerer.sharedModules;
const plans: SharedModulePlan[] = [];
lowerer.sharedModules = plans;
const compiled = new Map<string, ts.SourceFile>();
for (const sf of lowerer.moduleOrder) {
if (sf.isDeclarationFile) continue;
compiled.set(real(sf.fileName), sf);
}
// Island roots: requireModule() targets and island package files.
const queue: string[] = [];
for (const sf of lowerer.moduleOrder) {
const text = sf.text;
if (!text.includes("requireModule")) continue;
const from = real(sf.fileName);
for (const m of text.matchAll(REQUIRE_MODULE)) {
try {
queue.push(real(resolveFrom(from, m[1]!)));
} catch {
/* lowering reports it */
}
}
}
for (const island of lowerer.nodeIslands?.packages.values() ?? [])
packageJsFiles(island.dir, queue);
if (queue.length === 0) return plans;
const seen = new Set<string>();
const candidates = new Set<ts.SourceFile>();
// Classes island code extends, by the compiled module that exports them.
const extended = new Map<ts.SourceFile, string>();
while (queue.length > 0 && seen.size < MAX_ISLAND_FILES) {
const file = queue.pop()!;
if (seen.has(file) || compiled.has(file) || !isJsFile(file)) continue;
seen.add(file);
let text: string;
try {
text = readFileSync(file, "utf8");
} catch {
continue;
}
for (const re of SPECIFIERS) {
for (const m of text.matchAll(re)) {
for (const target of resolveAll(file, m[1]!)) {
const sf = compiled.get(target);
if (sf !== undefined) {
candidates.add(sf);
const base = extendedImport(text, m[1]!);
if (base !== null && !extended.has(sf)) extended.set(sf, base);
} else if (!seen.has(target)) queue.push(target);
}
}
}
}
if (candidates.size === 0) return plans;
const config = readProjectConfig(lowerer.entry.fileName);
for (const sf of lowerer.moduleOrder) {
if (!candidates.has(sf)) continue;
const path = real(sf.fileName);
const pkg = packageDirOf(path);
const loc: SrcLoc = { file: sf.fileName, start: 0, end: 0 };
const share = pkg === null ? "shared" : (config.packages.get(pkg.name)?.share ?? "auto");
const stateful = pkg === null ? "it is a program module" : statefulReason(sf);
const base = extended.get(sf);
if (base !== undefined) {
// Island code extends one of its classes: a native instance cannot
// take an island subclass's overrides, so the module cannot be owned
// by the program. A package then runs in Node for both sides
// (Node-owned, through handles; the build retries with it as an
// island); a program module is refused.
lowerer.pushDiag(sharedClassExtendedDiag(pkg?.name ?? null, sf.fileName, base, loc));
continue;
}
if (isCjsJsFile(sf, lowerer.program)) {
// Shortcut: CommonJS modules are not shared yet; each side keeps its
// copy, so a stateful one is reported.
if (stateful !== null)
lowerer.sharedModuleWarnings.push(
sharedModuleDuplicatedDiag(
pkg?.name ?? sf.fileName,
`${stateful}, and CommonJS modules cannot be shared yet`,
loc,
),
);
continue;
}
if (share === "duplicate" || (share === "auto" && stateful === null)) {
if (stateful !== null)
lowerer.sharedModuleWarnings.push(
sharedModuleDuplicatedDiag(
pkg?.name ?? sf.fileName,
`${stateful}, and it is marked "share": "duplicate"`,
loc,
),
);
continue;
}
const exports = moduleExports(sf);
for (const e of exports) {
if (e.kind !== "let" && e.kind !== "const") continue;
const flag = `%g.shared.${plans.length}.${e.name}%initialized`;
lowerer.globalsList.push({ id: flag, name: "%initialized", type: BOOL, mutable: true });
e.flag = flag;
let stmt: ts.Node | undefined = e.ident.parent;
while (stmt !== undefined && !ts.isVariableStatement(stmt)) stmt = stmt.parent;
if (stmt !== undefined) {
const list = lowerer.sharedInitFlags.get(stmt as ts.Statement) ?? [];
list.push(flag);
lowerer.sharedInitFlags.set(stmt as ts.Statement, list);
}
}
plans.push({ sf, path, exports });
}
if (plans.length > 0) lowerer.usesNodeIsland = true;
return plans;
}
/** The flag stores after a shared module's declaration statement. */
export function sharedInitFlagStores(lowerer: Lowerer, stmt: ts.Statement): IrStmt[] {
const flags = lowerer.sharedInitFlags.get(stmt);
if (flags === undefined) return [];
const loc = locOf(stmt);
return flags.map((flag) => ({
kind: "assign",
localId: flag,
value: { kind: "boolLit", value: true, type: BOOL, loc },
loc,
}));
}
/** The registrations the entry's %init runs first: one nodeIsland.share
* per shared module, with its accessor. */
export function sharedModuleRegistrations(lowerer: Lowerer): IrStmt[] {
const plans = sharedModulePlans(lowerer);
const out: IrStmt[] = [];
plans.forEach((plan, index) => {
const loc: SrcLoc = { file: plan.sf.fileName, start: 0, end: 0 };
const name = `%island.shared:${index}`;
const ctx = newFnCtx(false, null, null, DYN);
lowerer.fnStack.push(ctx);
const elems: IrExpr[] = [];
const kept: SharedExport[] = [];
try {
for (const e of plan.exports) {
let value: IrExpr;
try {
value = lowerer.coerceToExpected(lowerer.lowerExpr(e.ident), DYN);
} catch (error) {
if (!(error instanceof PoisonError)) throw error;
continue;
}
if (e.kind === "class") {
const builder = classMethodsBuilder(lowerer, e.ident, loc) ?? dynUndefinedExpr(loc);
const construct = classConstructHelper(lowerer, e.ident, loc) ?? dynUndefinedExpr(loc);
value = {
kind: "libCall",
fn: "nodeIsland.classMethodsOf",
args: [value, builder, construct],
type: DYN,
loc,
};
}
if (e.flag !== undefined) {
value = {
kind: "ternary",
cond: { kind: "varRef", localId: e.flag, type: BOOL, loc },
then: value,
else_: { kind: "libCall", fn: "nodeIsland.sharedTdz", args: [], type: DYN, loc },
type: DYN,
loc,
};
}
elems.push(value);
kept.push(e);
}
} finally {
lowerer.fnStack.pop();
}
lowerer.liftedFns.push({
name,
params: [],
returnType: DYN,
locals: ctx.locals,
body: [{ kind: "return", value: { kind: "dynArrLit", elems, type: DYN, loc }, loc }],
loc,
});
const exportsJson = JSON.stringify(kept.map((e) => ({ name: e.name, kind: e.kind })));
out.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "nodeIsland.share",
args: [
{ kind: "strLit", value: plan.path, type: STRING, loc },
{ kind: "strLit", value: exportsJson, type: STRING, loc },
lowerer.coerceToExpected(
{
kind: "closure",
fnName: name,
captures: [],
type: funcTypeFromParamShapes([], DYN),
loc,
},
DYN,
),
],
type: VOID,
loc,
},
loc,
});
});
return out;
}
@@ -1224,7 +1224,10 @@ function lowerToIrWithIslands(
}
// Divergence warnings over the program's own modules: computed once,
// after lowering, from the same checker (never part of a failure).
const divergences = scanDivergences(resultLowerer, moduleOrder);
const divergences = [
...scanDivergences(resultLowerer, moduleOrder),
...resultLowerer.sharedModuleWarnings,
];
timing("divergence-scan", { found: divergences.length });
if (divergences.length > 0) result = { ...result, divergences };
// The performance warning (SC6101): likely-hot functions of the program
@@ -3106,6 +3109,11 @@ export class Lowerer {
/** Island module loads, by the importing file and specifier: the module
* global holding what the import form sees (lower-node-island.ts). */
readonly islandModules = new Map<string, IslandModuleBinding>();
/** NI-6: the modules shared with Node islands (lower-shared-modules.ts),
* their SC8020 warnings, and the TDZ flags their declarations set. */
sharedModules: import("./lower-shared-modules.js").SharedModulePlan[] | null = null;
readonly sharedModuleWarnings: ScrDiagnostic[] = [];
readonly sharedInitFlags = new Map<ts.Statement, string[]>();
/** Per-directory answers of experimental-import.ts's declaration-file test. */
readonly experimentalFiles = new Map<string, boolean>();
/** Interned LifetimeRegion.run helpers, by callback type key. */
@@ -22,6 +22,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"nodeIsland.construct",
"nodeIsland.instanceof",
"nodeIsland.classMethods",
"nodeIsland.classMethodsOf",
// Node's argument, cipher-name, IV, tag-length and key-length errors.
"crypto.cipherNew",
// Node's curve, key, point, encoding and format errors.
@@ -204,6 +204,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** NI-5: a class instance crossing into an island, with the builder of
* its class's method table (run once per class). Answers the value. */
"nodeIsland.classMethods": { argTypes: [DYN, DYN], result: DYN },
/** NI-6 shared modules: registers a module the program shares with its
* islands (real path, exports JSON, accessor); the TDZ sentinel an
* accessor answers; the method table and constructor helper of an
* exported class by its class value (either may be undefined; answers
* the class value). */
"nodeIsland.share": { argTypes: [STRING, STRING, DYN], result: VOID },
"nodeIsland.sharedTdz": { argTypes: [], result: DYN },
"nodeIsland.classMethodsOf": { argTypes: [DYN, DYN, DYN], result: DYN },
/** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a
* checked-dynamic object (args: receiver, key string, value — all
* borrowed; the runtime copies the key and retains the value in). An
@@ -23,7 +23,7 @@ import { findInstalledLibnodePack, hostLibnodeTarget, libnodePackPackage } from
export const NODE_ISLAND_MIN_NODE = "24.13.1";
/** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */
export const NODE_BRIDGE_ABI = 6;
export const NODE_BRIDGE_ABI = 7;
/** The prefix of a recorded path relative to the executable's directory. */
export const ORIGIN = "$ORIGIN/";
@@ -272,7 +272,7 @@ globalThis.__scriptcNodeIsland = {
},
setTimer: noop, clearTimer: noop, refTimer: noop, refreshTimer: noop,
setImmediate: noop, clearImmediate: noop, refImmediate: noop,
nextTick: noop, emit: noop, load: noop,
nextTick: noop, emit: noop, load: noop, sharedPush: noop,
};
const fnToString = Function.prototype.toString;
const hasOwn = Object.prototype.hasOwnProperty;
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6850-node-island-shared-singleton/state.mjs",
"<repo>/tests/corpus/6850-node-island-shared-singleton/main.mjs"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6851-node-island-shared-class/shapes.mjs",
"<repo>/tests/corpus/6851-node-island-shared-class/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6852-node-island-shared-cycle/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6853-node-island-shared-registry/main.mjs"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6854-node-island-shared-registry-native/registry.mjs",
"<repo>/tests/corpus/6854-node-island-shared-registry-native/main.mjs"
],
"diags": []
}
@@ -70,6 +70,10 @@ Global<Function> g_set_timer, g_clear_timer, g_ref_timer, g_refresh_timer;
Global<Function> g_set_immediate, g_clear_immediate, g_ref_immediate;
Global<Function> g_next_tick, g_emit;
Global<Function> g_load; // the bootstrap's module loader (NI-4)
Global<Function> g_shared_push; // the loader hooks' binding updates (NI-6)
void SharedTable(const FunctionCallbackInfo<Value> &info);
void SharedGet(const FunctionCallbackInfo<Value> &info);
void SharedTdz(const FunctionCallbackInfo<Value> &info);
char *Copy(const std::string &text, size_t *len) {
char *out = static_cast<char *>(std::malloc(text.size() + 1));
@@ -237,6 +241,9 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
SetMethod(context, native, "settle", Settle);
SetMethod(context, native, "run", Run);
SetMethod(context, native, "listen", Listen);
SetMethod(context, native, "sharedTable", SharedTable);
SetMethod(context, native, "sharedGet", SharedGet);
SetMethod(context, native, "sharedTdz", SharedTdz);
global->Set(context, Utf8(isolate, "__scriptcNodeIslandHost"), native).Check();
TryCatch try_catch(isolate);
if (node::LoadEnvironment(g_setup->env(), bootstrap).IsEmpty()) {
@@ -269,7 +276,8 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
!TakeFunction(isolate, context, d, "refImmediate", &g_ref_immediate) ||
!TakeFunction(isolate, context, d, "nextTick", &g_next_tick) ||
!TakeFunction(isolate, context, d, "emit", &g_emit) ||
!TakeFunction(isolate, context, d, "load", &g_load))
!TakeFunction(isolate, context, d, "load", &g_load) ||
!TakeFunction(isolate, context, d, "sharedPush", &g_shared_push))
return Copy("the Node island bootstrap left an incomplete dispatcher", nullptr);
global->Delete(context, Utf8(isolate, "__scriptcNodeIsland")).FromMaybe(false);
global->Delete(context, Utf8(isolate, "__scriptcNodeIslandHost")).FromMaybe(false);
@@ -596,7 +604,7 @@ void Stop() {
DropAllExports();
for (Global<Function> *fn :
{&g_set_timer, &g_clear_timer, &g_ref_timer, &g_refresh_timer, &g_set_immediate,
&g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit})
&g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit, &g_shared_push})
fn->Reset();
isolate->Exit();
delete g_locker;
@@ -865,6 +873,14 @@ void ExportCall(const FunctionCallbackInfo<Value> &info) {
std::memset(&out, 0, sizeof out);
int status;
if (info.IsConstructCall()) {
// `class Sub extends NativeClass` in island code: the native instance
// cannot take the island prototype's overrides (NI-6 shortcut).
auto self = g_exports.find(id);
if (self != g_exports.end() && !info.NewTarget()->StrictEquals(self->second.Get(isolate))) {
isolate->ThrowException(v8::Exception::TypeError(Utf8(
isolate, "scriptc: island code cannot extend a native class of a shared module yet")));
return;
}
status = g_host->export_construct(id, args.data(), args.size(), &out);
} else {
ScrNodeValue self;
@@ -876,6 +892,15 @@ void ExportCall(const FunctionCallbackInfo<Value> &info) {
if (!result.IsEmpty()) info.GetReturnValue().Set(result);
}
void ExportHasInstance(const FunctionCallbackInfo<Value> &info) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
ScrNodeValue v;
FromV8(isolate, context, info[0], &v);
info.GetReturnValue().Set(g_host->export_instance_of(ExportIdArg(info.Data()), &v) == 1);
}
uint32_t HolderId(Local<Object> holder) {
if (holder->InternalFieldCount() < 1) return 0;
return ExportIdArg(holder->GetInternalField(0).As<Value>());
@@ -1055,6 +1080,14 @@ Local<Value> ExportValue(Isolate *isolate, Local<Context> context, const ScrNode
fn->SetName(in->u.exp.name != nullptr
? FromWtf8(isolate, in->u.exp.name, in->u.exp.name_len)
: String::Empty(isolate));
if (in->flags & SCR_NVF_CONSTRUCTOR) {
// A native class: instanceof asks the runtime (NI-6).
fn->DefineOwnProperty(context, v8::Symbol::GetHasInstance(isolate),
Function::New(context, ExportHasInstance, data, 1).ToLocalChecked(),
static_cast<v8::PropertyAttribute>(v8::DontEnum | v8::DontDelete |
v8::ReadOnly))
.Check();
}
obj = fn;
} else if (in->flags & SCR_NVF_PROMISE) {
Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
@@ -1515,6 +1548,56 @@ void DropHandle(uint32_t h) {
Drop(h);
}
// ── shared modules (NI-6) ──────────────────────────────────────────────
// Natives of the loader hooks (scr_node_island_hooks.cjs): the table of
// modules the program shares, the current value of one export, and the
// exports still in their temporal dead zone.
void SharedTable(const FunctionCallbackInfo<Value> &info) {
Isolate *isolate = info.GetIsolate();
HostCall scope;
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
g_host->shared_table(&out);
info.GetReturnValue().Set(FromWtf8(isolate, out.u.str.data, out.u.str.len));
}
void SharedTdz(const FunctionCallbackInfo<Value> &info) {
Isolate *isolate = info.GetIsolate();
HostCall scope;
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
g_host->shared_tdz(&out);
info.GetReturnValue().Set(FromWtf8(isolate, out.u.str.data, out.u.str.len));
}
// sharedGet(mod, slot, mode, tdz): the value, or `tdz` for a binding in its
// temporal dead zone.
void SharedGet(const FunctionCallbackInfo<Value> &info) {
Isolate *isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
HostCall scope;
ScrNodeValue out;
std::memset(&out, 0, sizeof out);
int status = g_host->shared_get(Uint32Arg(info, 0), Uint32Arg(info, 1),
static_cast<int>(Uint32Arg(info, 2)), &out);
if (status == 3) {
info.GetReturnValue().Set(info[3]);
return;
}
Local<Value> result;
ThrowOrReturn(isolate, context, status, &out, &result);
if (!result.IsEmpty()) info.GetReturnValue().Set(result);
}
void SharedPush(uint32_t mod, uint32_t slot, const ScrNodeValue *value) {
Enter e;
Local<Value> argv[] = {Integer::NewFromUnsigned(e.isolate, mod),
Integer::NewFromUnsigned(e.isolate, slot),
ToV8(e.isolate, e.context, value)};
Helper(e, g_shared_push, 3, argv);
}
const ScrNodeBridge kBridge = {
SCR_NODE_BRIDGE_ABI,
NODE_VERSION_STRING,
@@ -1551,6 +1634,7 @@ const ScrNodeBridge kBridge = {
DropHandle,
LiveHandles,
ExportSettle,
SharedPush,
};
} // namespace
+12
View File
@@ -2130,6 +2130,18 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) {
return scr_dyn_object_prototype();
}
if (object == scr_builtin_array_prototype) return scr_dyn_object_prototype();
#ifdef SCR_NODE_ISLANDS
/* An island object: its V8 prototype, through the __proto__ accessor
* (undefined for an object without Object.prototype: null). */
if (scr_ni_is_handle(object)) {
ScrDyn *proto = scr_ni_get_str(object, "__proto__", 9);
if (proto != NULL && proto->kind == SCR_DYN_UNDEF) {
scr_dyn_release(proto);
return scr_dyn_new_null();
}
return proto;
}
#endif
if (object->kind == SCR_DYN_NULL || object->kind == SCR_DYN_UNDEF) {
static const char message[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
+22 -1
View File
@@ -17,7 +17,7 @@
#include <stddef.h>
#include <stdint.h>
#define SCR_NODE_BRIDGE_ABI 6u
#define SCR_NODE_BRIDGE_ABI 7u
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
@@ -200,6 +200,24 @@ typedef struct ScrNodeBridgeHost {
void (*export_watch)(uint32_t id);
/* V8 collected the representative of `id`. */
void (*export_release)(uint32_t id);
/* `value instanceof <export id>` for a native class representative
* (its Symbol.hasInstance): 1 or 0. */
int (*export_instance_of)(uint32_t id, const ScrNodeValue *value);
/* ── shared modules (NI-6): modules the program compiled that island
* code imports resolve, through the loader hooks of the bootstrap, to
* generated modules over the live native exports. ── */
/* The table, as SCR_NV_STRING JSON: [{"path","exports":[{"name","kind"}]}]
* (index = module id, export index = slot). */
void (*shared_table)(ScrNodeValue *out);
/* The current value of a shared export. mode 0: read; 1: read and bind
* (the generated module holds it: later native changes are pushed with
* bridge->shared_push); 2: bind late (the binding is in its temporal
* dead zone: it is pushed once the native declaration ran). 0: *out is
* the value; 1: threw (*out thrown); 3: in its temporal dead zone. */
int (*shared_get)(uint32_t mod, uint32_t slot, int mode, ScrNodeValue *out);
/* The exports in their temporal dead zone now, as SCR_NV_STRING JSON
* [[mod, slot], ...]. */
void (*shared_tdz)(ScrNodeValue *out);
} ScrNodeBridgeHost;
typedef struct ScrNodeBridge {
@@ -290,6 +308,9 @@ typedef struct ScrNodeBridge {
uint32_t (*live_handles)(void);
/* Settles the V8 promise representing native promise `id` (NI-5). */
void (*export_settle)(uint32_t id, int rejected, const ScrNodeValue *value);
/* NI-6: the native value of shared export (mod, slot) changed: the
* generated module's binding takes `value`. */
void (*shared_push)(uint32_t mod, uint32_t slot, const ScrNodeValue *value);
} ScrNodeBridge;
#endif /* SCR_NODE_BRIDGE_H */
+8
View File
@@ -347,6 +347,9 @@ static void ni_on_node_stack(void (*job)(void *), void *arg) {
job(arg);
return;
}
/* Island code is about to run: it sees the current native values of
* shared exports (NI-6). */
scr_ni_shared_refresh();
if (ni_stack == NULL) {
ni_stack = mmap(NULL, NI_STACK, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (ni_stack == MAP_FAILED) ni_oom();
@@ -446,6 +449,7 @@ static void ni_host_exit(int code) {
static void ni_host_run(uint32_t kind, uint32_t token) {
NI_MARK_BEGIN;
scr_loop_hosted_run(kind, token);
scr_ni_shared_refresh();
NI_MARK_END;
}
@@ -489,6 +493,10 @@ static const ScrNodeBridgeHost ni_host = {
&scr_ni_host_export_keys,
&scr_ni_host_export_watch,
&scr_ni_host_export_release,
&scr_ni_host_export_instance_of,
&scr_ni_host_shared_table,
&scr_ni_host_shared_get,
&scr_ni_host_shared_tdz,
};
void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg) {
+11
View File
@@ -119,7 +119,18 @@ int scr_ni_host_export_delete(uint32_t id, const struct ScrNodeValue *key, struc
int scr_ni_host_export_keys(uint32_t id, struct ScrNodeValue *out);
void scr_ni_host_export_watch(uint32_t id);
void scr_ni_host_export_release(uint32_t id);
/* ── shared modules (scope-B NI-6, scr_node_island_export.c) ── */
void scr_node_island_share(ScrStr *path, ScrStr *exports, ScrDyn *accessor);
ScrDyn *scr_node_island_shared_tdz(void);
ScrDyn *scr_node_island_class_methods_of(ScrDyn *cls, ScrDyn *builder, ScrDyn *ctor);
void scr_ni_shared_refresh(void);
void scr_ni_exports_release_all(void); /* at exit, after Node stopped */
void *scr_ni_value_out_begin(ScrDyn *v, struct ScrNodeValue *out, bool *ok);
void scr_ni_value_out_end(void *keep);
int scr_ni_host_export_instance_of(uint32_t id, const struct ScrNodeValue *value);
void scr_ni_host_shared_table(struct ScrNodeValue *out);
int scr_ni_host_shared_get(uint32_t mod, uint32_t slot, int mode, struct ScrNodeValue *out);
void scr_ni_host_shared_tdz(struct ScrNodeValue *out);
/* 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));
+363 -11
View File
@@ -128,6 +128,8 @@ static void ni_ids_del(const void *key) {
static const void *ni_identity(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_FUNC:
/* A class value is boxed afresh per read: its identity is the class. */
if (v->v.fn.class_obj != NULL) return v->v.fn.class_obj;
return v->v.fn.clo != NULL ? (const void *)v->v.fn.clo : (const void *)v;
case SCR_DYN_PROMISE:
return v->v.promise;
@@ -141,7 +143,7 @@ static const void *ni_identity(const ScrDyn *v) {
uint32_t scr_ni_export_flags(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_FUNC:
return SCR_NVF_FUNCTION;
return v->v.fn.class_obj != NULL ? SCR_NVF_FUNCTION | SCR_NVF_CONSTRUCTOR : SCR_NVF_FUNCTION;
case SCR_DYN_PROMISE:
return SCR_NVF_PROMISE;
case SCR_DYN_ARR:
@@ -296,6 +298,7 @@ int scr_ni_host_export_call(uint32_t id, const ScrNodeValue *this_, const ScrNod
scr_dyn_release(self);
ni_args_free(in, argc, small);
scr_dyn_release(fn);
scr_ni_shared_refresh();
if (result == NULL || scr_exc_pending()) {
scr_dyn_release(result);
return ni_threw(out);
@@ -303,6 +306,8 @@ int scr_ni_host_export_call(uint32_t id, const ScrNodeValue *this_, const ScrNod
return ni_returned(result, out);
}
static ScrDyn *ni_class_ctor(const ScrDyn *cls);
int scr_ni_host_export_construct(uint32_t id, const ScrNodeValue *args, size_t argc,
ScrNodeValue *out) {
ni_keep_set(NULL);
@@ -312,13 +317,25 @@ int scr_ni_host_export_construct(uint32_t id, const ScrNodeValue *args, size_t a
return ni_threw(out);
}
scr_dyn_retain(ctor);
ScrDyn *list = scr_dyn_new_arr();
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(list, scr_ni_value_in(&args[i]));
ScrStr *what = scr_str_new("constructor", 11);
ScrDyn *result = scr_dyn_construct(ctor, list, what);
scr_str_release(what);
scr_dyn_release(list);
ScrDyn *result;
ScrDyn *helper = ni_class_ctor(ctor);
if (helper != NULL) {
/* A native class of a shared module (NI-6): its helper's thunk checks
* the arguments against the constructor's parameters. */
ScrDyn *small[8];
ScrDyn **in = ni_args_in(args, argc, small, 8);
result = scr_dyn_call(helper, in, argc, "constructor");
ni_args_free(in, argc, small);
} else {
ScrDyn *list = scr_dyn_new_arr();
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(list, scr_ni_value_in(&args[i]));
ScrStr *what = scr_str_new("constructor", 11);
result = scr_dyn_construct(ctor, list, what);
scr_str_release(what);
scr_dyn_release(list);
}
scr_dyn_release(ctor);
scr_ni_shared_refresh();
if (result == NULL || scr_exc_pending()) {
scr_dyn_release(result);
return ni_threw(out);
@@ -335,6 +352,7 @@ typedef struct {
const char *key;
size_t key_len;
ScrDyn *table; /* +1 dyn object */
ScrDyn *ctor; /* +1 dyn function constructing an instance (NI-6), or NULL */
} NiMethods;
static NiMethods *ni_methods = NULL;
static size_t ni_nmethods = 0, ni_methods_cap = 0;
@@ -363,11 +381,68 @@ ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder) {
ni_methods[ni_nmethods].key = value->v.typed_ref.type_key; /* static */
ni_methods[ni_nmethods].key_len = value->v.typed_ref.type_key_len;
ni_methods[ni_nmethods].table = table;
ni_methods[ni_nmethods].ctor = NULL;
ni_nmethods++;
}
return scr_dyn_retain(value);
}
/* NI-6: the method table of a class registered through its class value
* (a shared module's exported class), keyed by the same type key its
* instances carry. Answers the class value. */
/* The type key a class value's instances carry: "classval:C" -> "object:C".
* Answers whether `key`/`len` is it. */
static bool ni_instance_key_is(const char *sig, const char *key, size_t len) {
if (sig == NULL || key == NULL || strncmp(sig, "classval:", 9) != 0) return false;
size_t rest = strlen(sig + 9);
return len == 7 + rest && memcmp(key, "object:", 7) == 0 && memcmp(key + 7, sig + 9, rest) == 0;
}
static NiMethods *ni_methods_of_class(const ScrDyn *cls) {
if (cls->kind != SCR_DYN_FUNC || cls->v.fn.class_obj == NULL) return NULL;
for (size_t i = 0; i < ni_nmethods; i++)
if (ni_instance_key_is(cls->v.fn.sig, ni_methods[i].key, ni_methods[i].key_len))
return &ni_methods[i];
return NULL;
}
ScrDyn *scr_node_island_class_methods_of(ScrDyn *cls, ScrDyn *builder, ScrDyn *ctor) {
if (cls->kind != SCR_DYN_FUNC || cls->v.fn.class_obj == NULL || cls->v.fn.sig == NULL ||
strncmp(cls->v.fn.sig, "classval:", 9) != 0)
return scr_dyn_retain(cls);
NiMethods *known = ni_methods_of_class(cls);
if (known != NULL) {
if (known->ctor == NULL && ctor->kind == SCR_DYN_FUNC) known->ctor = scr_dyn_retain(ctor);
return scr_dyn_retain(cls);
}
ScrDyn *table = builder->kind == SCR_DYN_FUNC ? scr_dyn_call(builder, NULL, 0, "class methods")
: scr_dyn_new_obj();
if (table == NULL) return NULL;
if (ni_nmethods == ni_methods_cap) {
ni_methods_cap = ni_methods_cap ? ni_methods_cap * 2 : 8;
NiMethods *grown = realloc(ni_methods, ni_methods_cap * sizeof *grown);
if (grown == NULL) ni_export_oom();
ni_methods = grown;
}
size_t rest = strlen(cls->v.fn.sig + 9);
char *key = malloc(7 + rest + 1); /* lives as long as the program */
if (key == NULL) ni_export_oom();
memcpy(key, "object:", 7);
memcpy(key + 7, cls->v.fn.sig + 9, rest + 1);
ni_methods[ni_nmethods].key = key;
ni_methods[ni_nmethods].key_len = 7 + rest;
ni_methods[ni_nmethods].table = table;
ni_methods[ni_nmethods].ctor = ctor->kind == SCR_DYN_FUNC ? scr_dyn_retain(ctor) : NULL;
ni_nmethods++;
return scr_dyn_retain(cls);
}
/* The constructor helper of a native class value (borrowed), or NULL. */
static ScrDyn *ni_class_ctor(const ScrDyn *cls) {
NiMethods *m = ni_methods_of_class(cls);
return m != NULL ? m->ctor : NULL;
}
/* A prototype method of a class capsule (+1), or NULL. */
static ScrDyn *ni_class_method(const ScrDyn *target, const ScrDyn *key) {
if (key->kind != SCR_DYN_STR) return NULL;
@@ -390,8 +465,11 @@ int scr_ni_host_export_get(uint32_t id, const ScrNodeValue *key, ScrNodeValue *o
int present = 0;
if (pk->kind == SCR_DYN_STR) present = scr_dyn_in(target, pk->v.str);
if (!present && !scr_exc_pending()) {
/* A prototype method of a class instance (its table). */
ScrDyn *method = ni_class_method(target, pk);
scr_dyn_release(pk);
scr_dyn_release(k);
if (method != NULL) return ni_returned(method, out);
return 2;
}
ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_reflect_get(target, pk, target);
@@ -421,6 +499,7 @@ int scr_ni_host_export_set(uint32_t id, const ScrNodeValue *key, const ScrNodeVa
bool ok = scr_dyn_reflect_set(target, k, v, target);
scr_dyn_release(v);
scr_dyn_release(k);
scr_ni_shared_refresh();
if (scr_exc_pending()) return ni_threw(out);
(void)ok; /* a refused write is silent outside strict mode; Node's setter
* interceptor reports the same */
@@ -436,6 +515,11 @@ int scr_ni_host_export_has(uint32_t id, const ScrNodeValue *key) {
int present = 0;
if (pk != NULL && pk->kind == SCR_DYN_STR) present = scr_dyn_in(target, pk->v.str);
if (scr_exc_pending()) scr_exc_clear();
if (!present && pk != NULL) {
ScrDyn *method = ni_class_method(target, pk);
present = method != NULL;
scr_dyn_release(method);
}
scr_dyn_release(pk);
scr_dyn_release(k);
return present;
@@ -470,7 +554,9 @@ int scr_ni_host_export_keys(uint32_t id, ScrNodeValue *out) {
if (json == NULL || json->kind != SCR_DYN_STR) {
scr_dyn_release(json);
if (scr_exc_pending()) return ni_threw(out);
json = scr_dyn_new_str(scr_str_new("[]", 2));
ScrStr *empty = scr_str_new("[]", 2);
json = scr_dyn_new_str(empty);
scr_str_release(empty);
}
memset(out, 0, sizeof *out);
out->tag = SCR_NV_JSON;
@@ -497,6 +583,19 @@ void scr_ni_host_export_release(uint32_t id) {
scr_dyn_release(v);
}
/* `value instanceof C` for the representative of native class C: the
* value is an instance of C's own type key (NI-6 shortcut: instances of
* native subclasses of C answer false; NOTES.md). */
int scr_ni_host_export_instance_of(uint32_t id, const ScrNodeValue *value) {
ScrDyn *ctor = scr_ni_export_value(id);
if (ctor == NULL || ctor->kind != SCR_DYN_FUNC || ctor->v.fn.class_obj == NULL) return 0;
ScrDyn *v = scr_ni_value_in(value);
int is = v->kind == SCR_DYN_TYPED_REF &&
ni_instance_key_is(ctor->v.fn.sig, v->v.typed_ref.type_key, v->v.typed_ref.type_key_len);
scr_dyn_release(v);
return is;
}
/* ── promises into Node ───────────────────────────────────────────────── */
typedef struct {
@@ -567,8 +666,245 @@ void scr_ni_host_export_watch(uint32_t id) {
scr_promise_on_settle_sync(p, ni_promise_settled);
}
/* At exit, once Node stopped: everything the export table and the class
* tables hold (the RC audit counts them). */
/* ── shared modules (scope-B NI-6) ────────────────────────────────────
* A module the program compiled and island code also imports exists once:
* the program registers it (nodeIsland.share) with its accessor, a
* compiler-generated function answering the current values of its exports
* as a dyn array (the TDZ sentinel for a binding whose declaration has not
* run). The bootstrap's loader hooks (scr_node_island_hooks.cjs) resolve
* island imports of the module to a generated ES module whose bindings
* read those values; the bindings that can change (exported lets, and
* bindings still in their temporal dead zone when the generated module
* linked) are kept current by pushes: at each entry into Node and before
* each return from a native callback, the runtime compares the bound
* values with the last pushed ones and pushes the changes. Island code only
* runs inside those entries, so it never observes a stale binding. */
typedef struct {
ScrStr *path; /* +1 */
ScrStr *exports; /* +1 JSON [{"name","kind"}] */
ScrDyn *accessor; /* +1 */
uint32_t nslots;
ScrDyn **last; /* +1 each, NULL when unbound */
uint8_t *bound; /* 0 unbound, 1 bound, 2 bound late (not yet pushed) */
uint32_t nbound;
} NiShared;
static NiShared *ni_shared = NULL;
static uint32_t ni_nshared = 0, ni_shared_cap = 0;
static uint32_t ni_shared_bound = 0; /* bound slots over all modules */
static bool ni_refreshing = false;
static ScrDyn *ni_tdz = NULL;
static ScrDyn *ni_tdz_value(void) {
if (ni_tdz == NULL) ni_tdz = scr_dyn_new_obj();
return ni_tdz;
}
/* nodeIsland.sharedTdz: the sentinel an accessor answers for a binding in
* its temporal dead zone (+1). */
ScrDyn *scr_node_island_shared_tdz(void) { return scr_dyn_retain(ni_tdz_value()); }
/* nodeIsland.share(path, exports JSON, accessor): registers a shared
* module before any island code runs. Never loads Node. */
void scr_node_island_share(ScrStr *path, ScrStr *exports, ScrDyn *accessor) {
for (uint32_t i = 0; i < ni_nshared; i++)
if (ni_shared[i].path->len == path->len &&
memcmp(ni_shared[i].path->data, path->data, path->len) == 0)
return;
/* Released at exit (after Node stopped, when it started: atexit order). */
if (ni_nshared == 0 && ni_shared_cap == 0) scr_atexit(scr_ni_exports_release_all);
if (ni_nshared == ni_shared_cap) {
ni_shared_cap = ni_shared_cap ? ni_shared_cap * 2 : 8;
NiShared *grown = realloc(ni_shared, ni_shared_cap * sizeof *grown);
if (grown == NULL) ni_export_oom();
ni_shared = grown;
}
NiShared *m = &ni_shared[ni_nshared++];
memset(m, 0, sizeof *m);
m->path = scr_str_retain(path);
m->exports = scr_str_retain(exports);
m->accessor = scr_dyn_retain(accessor);
/* The slot count: the accessor's array length is the export count. */
uint32_t n = 0;
for (size_t i = 0; i < exports->len; i++) n += exports->data[i] == '{';
m->nslots = n;
m->last = calloc(n > 0 ? n : 1, sizeof *m->last);
m->bound = calloc(n > 0 ? n : 1, 1);
if (m->last == NULL || m->bound == NULL) ni_export_oom();
}
/* The current export values of module `m` (+1 dyn array), or NULL. */
static ScrDyn *ni_shared_values(NiShared *m) {
ScrDyn *values = scr_dyn_call(m->accessor, NULL, 0, "shared module");
if (values == NULL) scr_exc_clear();
return values;
}
void scr_ni_host_shared_table(ScrNodeValue *out) {
ni_keep_set(NULL);
size_t len = 2;
for (uint32_t i = 0; i < ni_nshared; i++)
len += ni_shared[i].path->len * 2 + ni_shared[i].exports->len + 32;
char *buf = malloc(len);
if (buf == NULL) ni_export_oom();
size_t at = 0;
buf[at++] = '[';
for (uint32_t i = 0; i < ni_nshared; i++) {
NiShared *m = &ni_shared[i];
if (i > 0) buf[at++] = ',';
at += (size_t)sprintf(buf + at, "{\"path\":\"");
for (size_t j = 0; j < m->path->len; j++) {
char c = m->path->data[j];
if (c == '"' || c == '\\') buf[at++] = '\\';
buf[at++] = c;
}
at += (size_t)sprintf(buf + at, "\",\"exports\":");
memcpy(buf + at, m->exports->data, m->exports->len);
at += m->exports->len;
buf[at++] = '}';
}
buf[at++] = ']';
ScrStr *text = scr_str_new(buf, at);
ScrDyn *json = scr_dyn_new_str(text);
scr_str_release(text);
free(buf);
memset(out, 0, sizeof *out);
out->tag = SCR_NV_STRING;
out->u.str.data = json->v.str->data;
out->u.str.len = json->v.str->len;
ni_keep_set(json);
}
static void ni_shared_bind(NiShared *m, uint32_t slot, ScrDyn *value, uint8_t how) {
if (m->bound[slot] == 0) {
m->nbound++;
ni_shared_bound++;
}
m->bound[slot] = how;
scr_dyn_release(m->last[slot]);
m->last[slot] = value != NULL ? scr_dyn_retain(value) : NULL;
}
int scr_ni_host_shared_get(uint32_t mod, uint32_t slot, int mode, ScrNodeValue *out) {
ni_keep_set(NULL);
memset(out, 0, sizeof *out);
if (mod >= ni_nshared || slot >= ni_shared[mod].nslots) return 3;
NiShared *m = &ni_shared[mod];
ScrDyn *values = ni_shared_values(m);
if (values == NULL) return 3;
ScrDyn *v = scr_dyn_arr_at(values, slot);
scr_dyn_release(values);
if (v == ni_tdz) {
scr_dyn_release(v);
if (mode == 2) ni_shared_bind(m, slot, NULL, 2);
return 3;
}
/* Arrays still cross as copies (NI-5): a copy of shared state would
* diverge silently, so the island read throws instead. */
if (v->kind == SCR_DYN_ARR ||
(v->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(v) != NULL)) {
scr_dyn_release(v);
static const char message[] =
"scriptc: an array exported by a shared module cannot cross into a Node island yet";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return ni_threw(out);
}
if (mode == 1 || mode == 2) ni_shared_bind(m, slot, v, 1);
return ni_returned(v, out);
}
void scr_ni_host_shared_tdz(ScrNodeValue *out) {
ni_keep_set(NULL);
size_t cap = 64, at = 0;
char *buf = malloc(cap);
if (buf == NULL) ni_export_oom();
buf[at++] = '[';
for (uint32_t i = 0; i < ni_nshared; i++) {
NiShared *m = &ni_shared[i];
ScrDyn *values = ni_shared_values(m);
if (values == NULL) continue;
for (uint32_t j = 0; j < m->nslots; j++) {
ScrDyn *v = scr_dyn_arr_at(values, j);
if (v == ni_tdz) {
if (at + 32 > cap) {
cap *= 2;
char *grown = realloc(buf, cap);
if (grown == NULL) ni_export_oom();
buf = grown;
}
at += (size_t)sprintf(buf + at, "%s[%u,%u]", at > 1 ? "," : "", i, j);
}
scr_dyn_release(v);
}
scr_dyn_release(values);
}
buf[at++] = ']';
ScrStr *text = scr_str_new(buf, at);
ScrDyn *json = scr_dyn_new_str(text);
scr_str_release(text);
free(buf);
memset(out, 0, sizeof *out);
out->tag = SCR_NV_STRING;
out->u.str.data = json->v.str->data;
out->u.str.len = json->v.str->len;
ni_keep_set(json);
}
typedef struct {
uint32_t mod, slot;
const ScrNodeValue *value;
} NiPush;
static void ni_push_job(void *p) {
NiPush *a = p;
scr_ni_bridge_loaded()->shared_push(a->mod, a->slot, a->value);
}
/* Pushes the bound exports whose native values changed since the last
* push. Cheap when nothing is bound. */
void scr_ni_shared_refresh(void) {
if (ni_shared_bound == 0 || ni_refreshing || scr_ni_bridge_loaded() == NULL) return;
ni_refreshing = true;
bool pending = scr_exc_pending();
ScrCaught *saved = pending ? scr_exc_take() : NULL;
for (uint32_t i = 0; i < ni_nshared; i++) {
NiShared *m = &ni_shared[i];
if (m->nbound == 0) continue;
ScrDyn *values = ni_shared_values(m);
if (values == NULL) continue;
for (uint32_t j = 0; j < m->nslots; j++) {
if (m->bound[j] == 0) continue;
ScrDyn *v = scr_dyn_arr_at(values, j);
if (v == ni_tdz || (m->bound[j] == 1 && m->last[j] != NULL && scr_dyn_strict_eq(v, m->last[j]))) {
scr_dyn_release(v);
continue;
}
ScrNodeValue nv;
bool ok = false;
void *keep = scr_ni_value_out_begin(v, &nv, &ok);
if (ok) {
NiPush a = {i, j, &nv};
scr_ni_on_node_stack(ni_push_job, &a);
} else {
scr_exc_clear();
}
scr_ni_value_out_end(keep);
scr_dyn_release(m->last[j]);
m->last[j] = v;
m->bound[j] = 1;
}
scr_dyn_release(values);
}
if (saved != NULL) {
scr_rethrow(saved);
scr_caught_release(saved);
}
ni_refreshing = false;
}
/* At exit, once Node stopped: everything the export table, the class
* tables and the shared modules hold (the RC audit counts them). */
void scr_ni_exports_release_all(void) {
ni_keep_set(NULL);
for (uint32_t i = 1; i < ni_exports_len; i++) {
@@ -581,8 +917,24 @@ void scr_ni_exports_release_all(void) {
free(ni_ids);
ni_ids = NULL;
ni_ids_cap = ni_ids_len = ni_ids_dead = 0;
for (size_t i = 0; i < ni_nmethods; i++) scr_dyn_release(ni_methods[i].table);
for (size_t i = 0; i < ni_nmethods; i++) {
scr_dyn_release(ni_methods[i].table);
scr_dyn_release(ni_methods[i].ctor);
}
ni_nmethods = 0;
for (uint32_t i = 0; i < ni_nshared; i++) {
NiShared *m = &ni_shared[i];
for (uint32_t j = 0; j < m->nslots; j++) scr_dyn_release(m->last[j]);
free(m->last);
free(m->bound);
scr_str_release(m->path);
scr_str_release(m->exports);
scr_dyn_release(m->accessor);
}
ni_nshared = 0;
ni_shared_bound = 0;
scr_dyn_release(ni_tdz);
ni_tdz = NULL;
}
#endif
+19 -1
View File
@@ -572,7 +572,10 @@ static bool nh_export_value(ScrDyn *v, ScrNodeValue *out, NhKeep *keep) {
scr_ni_export_out(v, out);
return true;
}
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_ARR) {
/* Arrays (dyn, and typed array capsules: NI-6 shortcut, as for dyn
* arrays) keep NI-4's JSON copy. */
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_ARR ||
(v->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(v) != NULL)) {
ScrStr *gap = scr_str_new("", 0);
ScrDyn *json = scr_json_stringify_replacer(v, scr_dyn_undefined(), gap);
scr_str_release(gap);
@@ -752,6 +755,7 @@ static void nh_call_job(void *p) {
}
static int nh_run(NhCall *c) {
scr_ni_shared_refresh();
scr_ni_on_node_stack(nh_call_job, c);
return c->threw;
}
@@ -1136,6 +1140,20 @@ bool scr_ni_value_out(ScrDyn *v, ScrNodeValue *out) {
return nh_to_node(v, out, &nh_out_keep);
}
/* NI-6: a conversion whose keep belongs to the caller, for values the
* runtime pushes into Node between other conversions (shared modules). */
void *scr_ni_value_out_begin(ScrDyn *v, ScrNodeValue *out, bool *ok) {
NhKeep *k = calloc(1, sizeof *k);
if (k == NULL) nh_oom();
*ok = nh_to_node(v, out, k);
return k;
}
void scr_ni_value_out_end(void *keep) {
nh_keep_done(keep);
free(keep);
}
ScrDyn *scr_ni_value_in(const ScrNodeValue *v) {
ScrDyn *d = nh_from_node(v);
return d != NULL ? d : scr_dyn_retain(scr_dyn_undefined());
@@ -0,0 +1,146 @@
// ── shared modules (scope-B NI-6) ──────────────────────────────────────
// The design's hooks.mjs. scripts/node-island-host.mjs appends this file to
// scr_node_island_host.cjs, so it runs in the bootstrap's scope (CommonJS:
// the bootstrap is Node's main script).
//
// A module the program compiled that island code imports must exist once.
// Synchronous, in-thread loader hooks (module.registerHooks) resolve every
// island import or require() of such a module to a generated ES module
// (served at the module's own file URL) whose bindings hold the live native exports: the
// natives sharedGet/sharedTable/sharedTdz ask the runtime directly. An
// export that can change (an exported let, or a binding still in its
// temporal dead zone when the module linked) is bound: the runtime pushes
// each new native value (sharedPush) before island code can observe it.
//
// Temporal dead zone across a cycle: when an island module loads while a
// shared module's bindings are not yet initialized (the native module is
// still evaluating, as in `core` -> island `ext` -> `core`), the load goes
// through an entry module that imports the island module and declares
// those bindings after the import. The generated module re-exports them
// from the entry, so while the island module evaluates they are in their
// temporal dead zone exactly as in Node (V8 throws its own ReferenceError
// with the importer's binding name), and they are pushed once the native
// declarations ran.
// oxlint-disable-next-line no-unused-vars -- called by scr_node_island_host.cjs, which this file is appended to
function scriptcSharedModules(nativeHost) {
const { registerHooks } = require("node:module");
const { fileURLToPath, pathToFileURL } = require("node:url");
const { dirname, join } = require("node:path");
const table = JSON.parse(nativeHost.sharedTable());
const none = { entry: (path, load) => load(null), push() {} };
if (table.length === 0) return none;
const byUrl = new Map();
table.forEach((m, id) => byUrl.set(pathToFileURL(m.path).href, id));
const TDZ = Symbol("scriptc.tdz");
const setters = new Map(); // `${mod}:${slot}` -> [(value) => void]
const entries = new Map(); // entry URL -> { target, late: [[mod, slot]] }
let entrySeq = 0;
let current = null; // the entry being loaded
function addSetter(mod, slot, set) {
const key = `${mod}:${slot}`;
const list = setters.get(key);
if (list === undefined) setters.set(key, [set]);
else list.push(set);
}
const shared = {
get(mod, slot) {
return nativeHost.sharedGet(mod, slot, 0, TDZ);
},
bind(mod, slot, set) {
addSetter(mod, slot, set);
return nativeHost.sharedGet(mod, slot, 1, TDZ);
},
late(mod, slot, set) {
addSetter(mod, slot, set);
const v = nativeHost.sharedGet(mod, slot, 2, TDZ);
return v === TDZ ? undefined : v;
},
};
Object.defineProperty(globalThis, Symbol.for("scriptc.shared"), { value: shared });
const lateName = (mod, slot) => `__scriptc_${mod}_${slot}`;
function sharedSource(mod) {
const m = table[mod];
const lines = ['const __s = globalThis[Symbol.for("scriptc.shared")];'];
const lateIn = current === null ? null : entries.get(current);
m.exports.forEach((e, slot) => {
const name = JSON.stringify(e.name);
const local = `__e${slot}`;
const tdz = nativeHost.sharedGet(mod, slot, 0, TDZ) === TDZ;
if (tdz && lateIn !== null && lateIn.late.some(([a, b]) => a === mod && b === slot)) {
lines.push(`export { ${lateName(mod, slot)} as ${name} } from ${JSON.stringify(current)};`);
} else if (tdz) {
// Reached outside an island load (a lazy require): no entry can hold
// the dead zone; the binding reads undefined until its declaration
// runs (NOTES.md shortcut).
lines.push(`let ${local} = __s.late(${mod}, ${slot}, (v) => { ${local} = v; });`);
lines.push(`export { ${local} as ${name} };`);
} else if (e.kind === "let" || e.kind === "var") {
lines.push(`let ${local} = __s.bind(${mod}, ${slot}, (v) => { ${local} = v; });`);
lines.push(`export { ${local} as ${name} };`);
} else {
lines.push(`const ${local} = __s.get(${mod}, ${slot});`);
lines.push(`export { ${local} as ${name} };`);
}
});
return lines.join("\n");
}
function entrySource(url) {
const e = entries.get(url);
const lines = [`import ${JSON.stringify(e.target)};`];
lines.push('const __s = globalThis[Symbol.for("scriptc.shared")];');
for (const [mod, slot] of e.late) {
const n = lateName(mod, slot);
lines.push(`export let ${n} = __s.late(${mod}, ${slot}, (v) => { ${n} = v; });`);
}
return lines.join("\n");
}
registerHooks({
resolve(specifier, context, nextResolve) {
const asUrl = specifier.startsWith("/") ? pathToFileURL(specifier).href : specifier;
if (entries.has(asUrl)) return { url: asUrl, shortCircuit: true };
const resolved = nextResolve(specifier, context);
// A shared module keeps its own file URL (require() and import key
// their module records alike only for file URLs); load answers it.
if (resolved.url.startsWith("file:") && byUrl.has(resolved.url))
return { url: resolved.url, format: "module", shortCircuit: true };
return resolved;
},
load(url, context, nextLoad) {
const mod = byUrl.get(url);
if (mod !== undefined)
return { format: "module", source: sharedSource(mod), shortCircuit: true };
if (entries.has(url))
return { format: "module", source: entrySource(url), shortCircuit: true };
return nextLoad(url, context);
},
});
return {
/** The entry to load the island module at `path` through, or null when
* no shared binding is in its temporal dead zone. Run `load` with it. */
entry(path, load) {
const late = JSON.parse(nativeHost.sharedTdz());
if (late.length === 0) return load(null);
// A file URL beside the island module (never on disk: the load hook
// answers it), so require() and import key it alike.
const url = pathToFileURL(join(dirname(path), `__scriptc_entry_${++entrySeq}.mjs`)).href;
entries.set(url, { target: pathToFileURL(path).href, late });
const outer = current;
current = url;
try {
return load(fileURLToPath(url));
} finally {
current = outer;
}
},
push(mod, slot, value) {
for (const set of setters.get(`${mod}:${slot}`) ?? []) set(value);
},
};
}
+15 -1
View File
@@ -127,10 +127,23 @@ function call(id, sync, file, specifier, name, argsJson) {
// answers a promise of the namespace.
const { pathToFileURL } = require("node:url");
// Island imports of modules the program shares resolve to their native
// exports (NI-6, scr_node_island_hooks.cjs, appended to this file).
let sharedModules = null;
function load(path, esm) {
if (path.startsWith("node:")) return require(path);
if (sharedModules === null) sharedModules = scriptcSharedModules(nativeHost);
if (esm) return import(pathToFileURL(path).href);
return createRequire(path)(path);
const load = createRequire(path);
return sharedModules.entry(path, (entry) => {
if (entry !== null) load(entry);
return load(path);
});
}
function sharedPush(mod, slot, value) {
if (sharedModules !== null) sharedModules.push(mod, slot, value);
}
// ── the hosted loop (scope-B NI-3) ─────────────────────────────────────
@@ -307,4 +320,5 @@ globalThis.__scriptcNodeIsland = {
nextTick,
emit,
load,
sharedPush,
};
+165 -3
View File
@@ -1,4 +1,5 @@
/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs.
/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs
* and scr_node_island_hooks.cjs.
* Do not edit: change the .cjs file and rerun the script. */
#ifndef SCR_NODE_ISLAND_HOST_H
#define SCR_NODE_ISLAND_HOST_H
@@ -133,10 +134,23 @@ static const char scr_node_island_host_js[] =
"// answers a promise of the namespace.\012"
"const { pathToFileURL } = require(\"node:url\");\012"
"\012"
"// Island imports of modules the program shares resolve to their native\012"
"// exports (NI-6, scr_node_island_hooks.cjs, appended to this file).\012"
"let sharedModules = null;\012"
"\012"
"function load(path, esm) {\012"
" if (path.startsWith(\"node:\")) return require(path);\012"
" if (sharedModules === null) sharedModules = scriptcSharedModules(nativeHost);\012"
" if (esm) return import(pathToFileURL(path).href);\012"
" return createRequire(path)(path);\012"
" const load = createRequire(path);\012"
" return sharedModules.entry(path, (entry) => {\012"
" if (entry !== null) load(entry);\012"
" return load(path);\012"
" });\012"
"}\012"
"\012"
"function sharedPush(mod, slot, value) {\012"
" if (sharedModules !== null) sharedModules.push(mod, slot, value);\012"
"}\012"
"\012"
"// \342\224\200\342\224\200 the hosted loop (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
@@ -313,6 +327,154 @@ static const char scr_node_island_host_js[] =
" nextTick,\012"
" emit,\012"
" load,\012"
"};\012";
" sharedPush,\012"
"};\012"
"\012"
"// \342\224\200\342\224\200 shared modules (scope-B NI-6) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
"// The design's hooks.mjs. scripts/node-island-host.mjs appends this file to\012"
"// scr_node_island_host.cjs, so it runs in the bootstrap's scope (CommonJS:\012"
"// the bootstrap is Node's main script).\012"
"//\012"
"// A module the program compiled that island code imports must exist once.\012"
"// Synchronous, in-thread loader hooks (module.registerHooks) resolve every\012"
"// island import or require() of such a module to a generated ES module\012"
"// (served at the module's own file URL) whose bindings hold the live native exports: the\012"
"// natives sharedGet/sharedTable/sharedTdz ask the runtime directly. An\012"
"// export that can change (an exported let, or a binding still in its\012"
"// temporal dead zone when the module linked) is bound: the runtime pushes\012"
"// each new native value (sharedPush) before island code can observe it.\012"
"//\012"
"// Temporal dead zone across a cycle: when an island module loads while a\012"
"// shared module's bindings are not yet initialized (the native module is\012"
"// still evaluating, as in `core` -> island `ext` -> `core`), the load goes\012"
"// through an entry module that imports the island module and declares\012"
"// those bindings after the import. The generated module re-exports them\012"
"// from the entry, so while the island module evaluates they are in their\012"
"// temporal dead zone exactly as in Node (V8 throws its own ReferenceError\012"
"// with the importer's binding name), and they are pushed once the native\012"
"// declarations ran.\012"
"// oxlint-disable-next-line no-unused-vars -- called by scr_node_island_host.cjs, which this file is appended to\012"
"function scriptcSharedModules(nativeHost) {\012"
" const { registerHooks } = require(\"node:module\");\012"
" const { fileURLToPath, pathToFileURL } = require(\"node:url\");\012"
" const { dirname, join } = require(\"node:path\");\012"
" const table = JSON.parse(nativeHost.sharedTable());\012"
" const none = { entry: (path, load) => load(null), push() {} };\012"
" if (table.length === 0) return none;\012"
" const byUrl = new Map();\012"
" table.forEach((m, id) => byUrl.set(pathToFileURL(m.path).href, id));\012"
" const TDZ = Symbol(\"scriptc.tdz\");\012"
" const setters = new Map(); // `${mod}:${slot}` -> [(value) => void]\012"
" const entries = new Map(); // entry URL -> { target, late: [[mod, slot]] }\012"
" let entrySeq = 0;\012"
" let current = null; // the entry being loaded\012"
"\012"
" function addSetter(mod, slot, set) {\012"
" const key = `${mod}:${slot}`;\012"
" const list = setters.get(key);\012"
" if (list === undefined) setters.set(key, [set]);\012"
" else list.push(set);\012"
" }\012"
"\012"
" const shared = {\012"
" get(mod, slot) {\012"
" return nativeHost.sharedGet(mod, slot, 0, TDZ);\012"
" },\012"
" bind(mod, slot, set) {\012"
" addSetter(mod, slot, set);\012"
" return nativeHost.sharedGet(mod, slot, 1, TDZ);\012"
" },\012"
" late(mod, slot, set) {\012"
" addSetter(mod, slot, set);\012"
" const v = nativeHost.sharedGet(mod, slot, 2, TDZ);\012"
" return v === TDZ \077 undefined : v;\012"
" },\012"
" };\012"
" Object.defineProperty(globalThis, Symbol.for(\"scriptc.shared\"), { value: shared });\012"
"\012"
" const lateName = (mod, slot) => `__scriptc_${mod}_${slot}`;\012"
"\012"
" function sharedSource(mod) {\012"
" const m = table[mod];\012"
" const lines = ['const __s = globalThis[Symbol.for(\"scriptc.shared\")];'];\012"
" const lateIn = current === null \077 null : entries.get(current);\012"
" m.exports.forEach((e, slot) => {\012"
" const name = JSON.stringify(e.name);\012"
" const local = `__e${slot}`;\012"
" const tdz = nativeHost.sharedGet(mod, slot, 0, TDZ) === TDZ;\012"
" if (tdz && lateIn !== null && lateIn.late.some(([a, b]) => a === mod && b === slot)) {\012"
" lines.push(`export { ${lateName(mod, slot)} as ${name} } from ${JSON.stringify(current)};`);\012"
" } else if (tdz) {\012"
" // Reached outside an island load (a lazy require): no entry can hold\012"
" // the dead zone; the binding reads undefined until its declaration\012"
" // runs (NOTES.md shortcut).\012"
" lines.push(`let ${local} = __s.late(${mod}, ${slot}, (v) => { ${local} = v; });`);\012"
" lines.push(`export { ${local} as ${name} };`);\012"
" } else if (e.kind === \"let\" || e.kind === \"var\") {\012"
" lines.push(`let ${local} = __s.bind(${mod}, ${slot}, (v) => { ${local} = v; });`);\012"
" lines.push(`export { ${local} as ${name} };`);\012"
" } else {\012"
" lines.push(`const ${local} = __s.get(${mod}, ${slot});`);\012"
" lines.push(`export { ${local} as ${name} };`);\012"
" }\012"
" });\012"
" return lines.join(\"\\n\");\012"
" }\012"
"\012"
" function entrySource(url) {\012"
" const e = entries.get(url);\012"
" const lines = [`import ${JSON.stringify(e.target)};`];\012"
" lines.push('const __s = globalThis[Symbol.for(\"scriptc.shared\")];');\012"
" for (const [mod, slot] of e.late) {\012"
" const n = lateName(mod, slot);\012"
" lines.push(`export let ${n} = __s.late(${mod}, ${slot}, (v) => { ${n} = v; });`);\012"
" }\012"
" return lines.join(\"\\n\");\012"
" }\012"
"\012"
" registerHooks({\012"
" resolve(specifier, context, nextResolve) {\012"
" const asUrl = specifier.startsWith(\"/\") \077 pathToFileURL(specifier).href : specifier;\012"
" if (entries.has(asUrl)) return { url: asUrl, shortCircuit: true };\012"
" const resolved = nextResolve(specifier, context);\012"
" // A shared module keeps its own file URL (require() and import key\012"
" // their module records alike only for file URLs); load answers it.\012"
" if (resolved.url.startsWith(\"file:\") && byUrl.has(resolved.url))\012"
" return { url: resolved.url, format: \"module\", shortCircuit: true };\012"
" return resolved;\012"
" },\012"
" load(url, context, nextLoad) {\012"
" const mod = byUrl.get(url);\012"
" if (mod !== undefined)\012"
" return { format: \"module\", source: sharedSource(mod), shortCircuit: true };\012"
" if (entries.has(url))\012"
" return { format: \"module\", source: entrySource(url), shortCircuit: true };\012"
" return nextLoad(url, context);\012"
" },\012"
" });\012"
"\012"
" return {\012"
" /** The entry to load the island module at `path` through, or null when\012"
" * no shared binding is in its temporal dead zone. Run `load` with it. */\012"
" entry(path, load) {\012"
" const late = JSON.parse(nativeHost.sharedTdz());\012"
" if (late.length === 0) return load(null);\012"
" // A file URL beside the island module (never on disk: the load hook\012"
" // answers it), so require() and import key it alike.\012"
" const url = pathToFileURL(join(dirname(path), `__scriptc_entry_${++entrySeq}.mjs`)).href;\012"
" entries.set(url, { target: pathToFileURL(path).href, late });\012"
" const outer = current;\012"
" current = url;\012"
" try {\012"
" return load(fileURLToPath(url));\012"
" } finally {\012"
" current = outer;\012"
" }\012"
" },\012"
" push(mod, slot, value) {\012"
" for (const set of setters.get(`${mod}:${slot}`) \077\077 []) set(value);\012"
" },\012"
" };\012"
"}\012";
#endif /* SCR_NODE_ISLAND_HOST_H */
@@ -8,6 +8,6 @@ const srcDir = join(import.meta.dirname, "../src");
// scr_node_island.c starts `node -e` with the host embedded in this header;
// a host edit without regenerating it would ship the old host.
test("the embedded Node island host matches its source", () => {
const source = readFileSync(join(srcDir, "scr_node_island_host.cjs"), "utf8");
const source = `${readFileSync(join(srcDir, "scr_node_island_host.cjs"), "utf8")}\n${readFileSync(join(srcDir, "scr_node_island_hooks.cjs"), "utf8")}`;
expect(readFileSync(join(srcDir, "scr_node_island_host.h"), "utf8")).toBe(renderHeader(source));
});
+5 -2
View File
@@ -10,6 +10,8 @@ import { fileURLToPath } from "node:url";
const srcDir = fileURLToPath(new URL("../packages/runtime/src/", import.meta.url));
const SOURCE = `${srcDir}scr_node_island_host.cjs`;
// The loader hooks of shared modules (NI-6) run in the bootstrap's scope.
const HOOKS = `${srcDir}scr_node_island_hooks.cjs`;
const HEADER = `${srcDir}scr_node_island_host.h`;
/** One C string literal per source line, every byte outside printable
@@ -28,7 +30,8 @@ export function renderHeader(source) {
const lines = source.split("\n");
if (lines.at(-1) === "") lines.pop();
const body = lines.map((line) => ` ${cLiteral(`${line}\n`)}`).join("\n");
return `/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs.
return `/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs
* and scr_node_island_hooks.cjs.
* Do not edit: change the .cjs file and rerun the script. */
#ifndef SCR_NODE_ISLAND_HOST_H
#define SCR_NODE_ISLAND_HOST_H
@@ -41,7 +44,7 @@ ${body};
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const expected = renderHeader(readFileSync(SOURCE, "utf8"));
const expected = renderHeader(`${readFileSync(SOURCE, "utf8")}\n${readFileSync(HOOKS, "utf8")}`);
if (process.argv.includes("--check")) {
if (readFileSync(HEADER, "utf8") !== expected) {
console.error(`${HEADER} is stale: run node scripts/node-island-host.mjs`);
@@ -0,0 +1,28 @@
// @requires-libnode
// A module both the program and a Node island import exists once (scope-B
// NI-6): island imports of the compiled `state.mjs` resolve to its live
// native exports, so a write on either side is seen by the other, and an
// exported `let` is a live binding in the island too.
import { requireModule } from "@scriptc/node";
import { config, count, entries, record, reset } from "./state.mjs";
record("main-1");
console.log("main before island", count, entries());
const plugin = requireModule("./plugin.mjs");
console.log("island start:", plugin.startup);
console.log("main after island load", count, entries());
console.log("island report:", plugin.report());
record("main-2");
console.log("island report after main write:", plugin.report());
console.log("island bump:", plugin.bump("island-2"), "main sees", count);
plugin.configure("island-mode");
console.log("main sees config", config.mode, config.owner);
config.owner = "main";
console.log("island sees config", plugin.describeConfig());
console.log("same functions", plugin.sameRecord(record), plugin.requireSees());
reset();
console.log("after reset", count, entries(), plugin.report());
@@ -0,0 +1,37 @@
// Island code (loaded with requireModule): imports the shared module by
// named imports, by namespace, and through require().
import { createRequire } from "node:module";
import * as ns from "./state.mjs";
import { config, count, entries, record } from "./state.mjs";
const require = createRequire(import.meta.url);
record("island-1");
export const startup = `count=${count} ns=${ns.count} entries=${entries()}`;
export function report() {
return `count=${count} ns=${ns.count} entries=${entries()}`;
}
export function bump(name) {
const before = count;
const after = record(name);
return `${before}->${after} live=${count}`;
}
export function configure(mode) {
config.mode = mode;
}
export function describeConfig() {
return `${config.mode}/${config.owner}`;
}
export function sameRecord(fn) {
return fn === record && fn === ns.record;
}
export function requireSees() {
const viaRequire = require("./state.mjs");
return viaRequire === ns && viaRequire.entries() === entries();
}
@@ -0,0 +1,19 @@
// A stateful singleton: a module-level log and an exported counter.
export let count = 0;
const log = [];
export const config = { mode: "default", owner: "state" };
export function record(entry) {
log.push(entry);
count++;
return count;
}
export function entries() {
return log.join(",");
}
export function reset() {
log.length = 0;
count = 0;
}
@@ -0,0 +1,35 @@
import { Point, isPoint, make, remember, remembered } from "./shapes.mjs";
export function check(p) {
return `${p instanceof Point} ${isPoint(p)} ${p.describe()} norm=${p.norm()}`;
}
export function create(x, y) {
return new Point(x, y);
}
export function selfCheck() {
const p = new Point(1, 1);
return `${p instanceof Point} ${isPoint(p)} ${make(2, 2) instanceof Point}`;
}
export function plainIsPoint() {
return `${{ x: 1 } instanceof Point} ${isPoint({ x: 1 })}`;
}
export function keep(p) {
return remember(p);
}
export function firstIsNative(p) {
return remembered(0) === p;
}
export function viaFactory() {
const p = make(5, 12);
return `${p.describe()} ${p.norm()} ${p instanceof Point}`;
}
export function sameClass(c) {
return c === Point;
}
@@ -0,0 +1,23 @@
// @requires-libnode
// A class exported by a module both sides import is one class (scope-B
// NI-6): instances made on either side answer instanceof on both, keep
// their identity, and their methods run natively.
import { requireModule } from "@scriptc/node";
import { Point, describeAll, isPoint, make, remember, remembered } from "./shapes.mjs";
const island = requireModule("./island.mjs");
const native = new Point(3, 4);
console.log("island sees native instance:", island.check(native));
const fromIsland = island.create(6, 8);
console.log("native instanceof:", fromIsland instanceof Point, isPoint(fromIsland));
console.log("native reads:", fromIsland.x, fromIsland.y, fromIsland.norm());
console.log("island instanceof:", island.selfCheck());
console.log("plain object (island):", island.plainIsPoint());
console.log("plain object (native):", isPoint({ x: 1, y: 2 }));
remember(native);
console.log("kept:", island.keep(fromIsland), describeAll());
console.log("identity:", remembered(1) === fromIsland, island.firstIsNative(native));
console.log("made by native factory in island:", island.viaFactory());
console.log("class identity:", island.sameClass(Point), island.sameClass(make));
@@ -0,0 +1,35 @@
export class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
norm() {
return Math.hypot(this.x, this.y);
}
describe() {
return `(${this.x},${this.y})`;
}
}
const kept = [];
export function remember(p) {
kept.push(p);
return kept.length;
}
export function remembered(i) {
return kept[i];
}
export function describeAll() {
return kept.map((p) => p.describe()).join(" ");
}
export function make(x, y) {
return new Point(x, y);
}
export function isPoint(p) {
return p instanceof Point;
}
@@ -0,0 +1,10 @@
// @requires-libnode
// An import cycle through a Node island (scope-B NI-6): the compiled package
// `core` imports the island package `ext`, which imports `core` back. As in
// Node, `ext` evaluates first and sees core's functions (hoisted) but its
// let and const bindings in their temporal dead zone; once core's body ran,
// the same bindings read core's live values.
import { check, x } from "core";
console.log("main:", check());
console.log("main x:", x);
@@ -0,0 +1,5 @@
export let x: number;
export const y: string;
export function f(): string;
export function bump(): number;
export function check(): string;
+16
View File
@@ -0,0 +1,16 @@
import { early, later, bumpFromIsland } from "ext";
console.log("core body starts; ext saw:", early);
export let x = 1;
export const y = "y";
export function f() {
return "f";
}
export function bump() {
x++;
return x;
}
export function check() {
return `${later()} ${bumpFromIsland()} ${later()}`;
}
@@ -0,0 +1 @@
{ "name": "core", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,3 @@
export const early: string;
export function later(): string;
export function bumpFromIsland(): string;
+19
View File
@@ -0,0 +1,19 @@
import { bump, f, x, y } from "core";
console.log("ext evaluates; f() =", f());
function probe(read) {
try {
return String(read());
} catch (e) {
return `${e.name}: ${e.message}`;
}
}
export const early = `${probe(() => x)} | ${probe(() => y)}`;
export function later() {
return `x=${x} y=${y}`;
}
export function bumpFromIsland() {
return `bump=${bump()} x=${x}`;
}
@@ -0,0 +1 @@
{ "name": "ext", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "shared-cycle", "private": true, "type": "module", "scriptc": { "packages": { "ext": "node" } } }
@@ -0,0 +1,19 @@
// @requires-libnode
// The registry scenario of the Node delegation design (§13): a stateful
// package — a module-level Map, an exported `let counter`, a class — that
// the program and a Node island both import. Island code subclasses the
// package's class, so the package is shared Node-owned (scope-B NI-6): one
// instance, its state visible on both sides, live bindings, `require` and
// `import` answering the same module, instanceof both ways, and an island
// subclass whose override and `super` call cross the boundary.
import fs from "node:fs";
import { requireModule } from "@scriptc/node";
import * as reg from "registry";
const { run } = requireModule("./scenario.mjs");
const log = (s) => fs.writeSync(1, `${s}\n`);
reg.register(new reg.Plugin("owner-a"));
run(log);
log(`owner list: ${reg.list().join(",")}`);
log(`owner counter: ${reg.counter}`);
log(`owner describeAll: ${reg.describeAll()}`);
@@ -0,0 +1,13 @@
export let counter: number;
export function bump(): number;
export class Plugin {
name: string;
constructor(name: string);
describe(): string;
}
export function register(p: { name: string; describe(): string }): number;
// A handle (NI-4 copies no island arrays into typed native arrays).
export function list(): any;
export function find(name: string): Plugin | undefined;
export function describeAll(): string;
export function noop<T>(x: T): T;
@@ -0,0 +1,13 @@
// A stateful singleton: a plugin registry with a mutable exported binding.
export let counter = 0;
export function bump() { return ++counter; }
const items = new Map();
export class Plugin {
constructor(name) { this.name = name; }
describe() { return `plugin:${this.name}`; }
}
export function register(p) { items.set(p.name, p); bump(); return items.size; }
export function list() { return [...items.keys()]; }
export function find(name) { return items.get(name); }
export function describeAll() { return [...items.values()].map((p) => p.describe()).join(' | '); }
export function noop(x) { return x; }
@@ -0,0 +1 @@
{ "name": "registry", "version": "1.0.0", "type": "module", "exports": { "types": "./index.d.ts", "default": "./index.js" } }
@@ -0,0 +1,26 @@
// Island code. Imports the shared module both as ESM (named + namespace) and via require().
import { createRequire } from 'node:module';
import * as ns from 'registry';
import { counter, bump, Plugin, register, list, find, describeAll, noop } from 'registry';
const require = createRequire(import.meta.url);
export function run(log) {
log(`island list: ${list().join(',')}`);
log(`island counter: named=${counter} ns=${ns.counter}`);
bump();
log(`after island bump: named=${counter} ns=${ns.counter}`);
log(`require() sees same instance: ${require('registry').list().join(',')}`);
const p = new Plugin('island-b');
register(p);
log(`instanceof (island-constructed): ${p instanceof Plugin}`);
log(`instanceof (owner-constructed): ${find('owner-a') instanceof Plugin}`);
log(`find returns same object twice: ${find('owner-a') === find('owner-a')}`);
class Sub extends Plugin { describe() { return `sub(${super.describe()})`; } }
const sub = new Sub('island-sub');
log(`subclass: proto ok=${Object.getPrototypeOf(sub) === Sub.prototype} describe=${sub.describe()} instanceof Plugin=${sub instanceof Plugin}`);
register(sub);
register({ name: 'island-plain', describe() { return 'plain-from-island'; } });
log(`describeAll from island: ${describeAll()}`);
log(`Object.keys(owner object): ${Object.keys(find('owner-a') ?? {}).join(',')}`);
}
@@ -0,0 +1,18 @@
// @requires-libnode
// The registry scenario of the Node delegation design (§13) with the
// registry as a program module, shared through the loader hooks over its
// native exports (scope-B NI-6): one instance, its state visible on both
// sides, live bindings, `require` and `import` answering the same module,
// instanceof both ways, and island objects in the native registry. (The
// design's island subclass is in 6843, where the registry is Node-owned.)
import fs from "node:fs";
import { requireModule } from "@scriptc/node";
import * as reg from "./registry.mjs";
const { run } = requireModule("./scenario.mjs");
const log = (s) => fs.writeSync(1, `${s}\n`);
reg.register(new reg.Plugin("owner-a"));
run(log);
log(`owner list: ${reg.list().join(",")}`);
log(`owner counter: ${reg.counter}`);
log(`owner describeAll: ${reg.describeAll()}`);
@@ -0,0 +1,13 @@
// A stateful singleton: a plugin registry with a mutable exported binding.
export let counter = 0;
export function bump() { return ++counter; }
const items = new Map();
export class Plugin {
constructor(name) { this.name = name; }
describe() { return `plugin:${this.name}`; }
}
export function register(p) { items.set(p.name, p); bump(); return items.size; }
export function list() { return [...items.keys()]; }
export function find(name) { return items.get(name); }
export function describeAll() { return [...items.values()].map((p) => p.describe()).join(' | '); }
export function noop(x) { return x; }
@@ -0,0 +1,22 @@
// Island code. Imports the shared module both as ESM (named + namespace) and via require().
import { createRequire } from 'node:module';
import * as ns from './registry.mjs';
import { counter, bump, Plugin, register, list, find, describeAll, noop } from './registry.mjs';
const require = createRequire(import.meta.url);
export function run(log) {
log(`island list: ${list().join(',')}`);
log(`island counter: named=${counter} ns=${ns.counter}`);
bump();
log(`after island bump: named=${counter} ns=${ns.counter}`);
log(`require() sees same instance: ${require('./registry.mjs').list().join(',')}`);
const p = new Plugin('island-b');
register(p);
log(`instanceof (island-constructed): ${p instanceof Plugin}`);
log(`instanceof (owner-constructed): ${find('owner-a') instanceof Plugin}`);
log(`find returns same object twice: ${find('owner-a') === find('owner-a')}`);
register({ name: 'island-plain', describe() { return 'plain-from-island'; } });
log(`describeAll from island: ${describeAll()}`);
log(`Object.keys(owner object): ${Object.keys(find('owner-a') ?? {}).join(',')}`);
}
@@ -0,0 +1 @@
{ "plain": 2500 }
@@ -0,0 +1 @@
{ "plain": 2500 }
@@ -0,0 +1 @@
{ "plain": 2500 }
@@ -0,0 +1 @@
{ "plain": 2500 }
@@ -0,0 +1 @@
{ "plain": 2500 }
+1 -1
View File
@@ -160,7 +160,7 @@ describe.skipIf(skip)("Node islands in-process", () => {
const result = await run(binary, [], { ...process.env, SCRIPTC_LIBNODE: dir });
expect(result.code).toBe(1);
expect(result.stderr).toBe(
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 6)\n`,
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 7)\n`,
);
});