mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
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:
co-authored by
Claude Opus 5.5
parent
2f8e8b2b92
commit
5fa44d9243
@@ -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;
|
||||
|
||||
+7
@@ -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": []
|
||||
}
|
||||
+7
@@ -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": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6852-node-island-shared-cycle/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6853-node-island-shared-registry/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
+5
@@ -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
@@ -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()}`;
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "core", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const early: string;
|
||||
export function later(): string;
|
||||
export function bumpFromIsland(): string;
|
||||
+19
@@ -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}`;
|
||||
}
|
||||
+1
@@ -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()}`);
|
||||
+13
@@ -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;
|
||||
+13
@@ -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; }
|
||||
+1
@@ -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 }
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user