Merge remote-tracking branch 'origin/main' into fix/pi-wp6b-stdlib-tail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-28 11:57:03 -07:00
co-authored by Claude Opus 5.5
98 changed files with 3077 additions and 85 deletions
@@ -22,6 +22,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -306,6 +306,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.builtinModule": "scr_process_builtin_module",
"process.builtinUnsupported": "scr_process_builtin_unsupported",
"module.namespaceValue": "scr_module_namespace_value",
"module.namespaceDyn": "scr_module_namespace_dyn_value",
"fs.callbackValue": "scr_fs_callback_value",
"fs.callbackCall": "scr_fs_callback_call",
"process.hrtimeValue": "scr_process_hrtime_value",
@@ -1060,6 +1061,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",
"nodeIsland.copyArrays": "scr_node_island_copy_arrays",
"nodeIsland.awaitCopy": "scr_node_island_await_copy",
"dyn.keySet": "scr_dyn_key_set",
@@ -40,6 +40,9 @@ export interface NativeLinkFeatures {
/** Module namespace objects as checked-dynamic values link
* scr_module_namespace.c; absent (false) otherwise. */
moduleNamespace?: boolean;
/** A module namespace boxed for typed code links
* scr_module_namespace_dyn.c; absent (false) otherwise. */
moduleNamespaceDyn?: boolean;
zlib: boolean;
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
brotli?: boolean;
@@ -164,6 +164,9 @@ export interface CcOptions {
/** Module namespace objects as checked-dynamic values: compiles
* scr_module_namespace.c. */
moduleNamespace?: boolean;
/** A module namespace boxed for typed code: compiles
* scr_module_namespace_dyn.c. */
moduleNamespaceDyn?: boolean;
/** The program uses the process-events surface (signal/exit listeners,
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
* IR): compiles scr_events.c into the binary. Event-free binaries keep
@@ -202,6 +202,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.wasm, "scr_wasm.c"),
// Module namespace objects as checked-dynamic values.
unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"),
// A module namespace as the frozen null-prototype object typed code sees.
unit((u) => u.opts.moduleNamespaceDyn === true, "scr_module_namespace_dyn.c"),
unit(
(u) =>
u.opts.dynAsync ||
@@ -103,6 +103,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
nodeIsland: boolean;
brotli: boolean;
moduleNamespace: boolean;
moduleNamespaceDyn: boolean;
nativeFetch: boolean;
abortSignalOnly: boolean;
netEffective: boolean;
@@ -312,6 +313,7 @@ export function effectiveRuntimeFeatures(
nodeIsland: features.nodeIsland === true,
brotli: features.brotli === true,
moduleNamespace: features.moduleNamespace === true,
moduleNamespaceDyn: features.moduleNamespaceDyn === true,
nativeFetch,
// The fetch unit has AbortController and AbortSignal; scr_abort.c is
// the same implementation without the fetch transport.
+2 -2
View File
@@ -116,6 +116,8 @@ export const DIAGNOSTIC_CODES = {
SC8006: "Node island modules the build cannot collect",
SC8010: "value crossing a Node island boundary in a form without an exact copy",
SC8011: "native class extending a Node island class outside the supported forms",
SC8020: "stateful module duplicated across the Node island boundary",
SC8022: "island code extends a class of a compiled module",
SC8030: "type patch: no range covers the installed copy",
SC8031: "type patch: selector matches nothing",
SC8032: "type patch: declaration already has the type",
@@ -154,9 +156,7 @@ export const RETIRED_DIAGNOSTIC_CODES = {
* abandoned, delete them here. */
export const RESERVED_DIAGNOSTIC_CODES = {
SC8012: "Node islands (ni/* branches)",
SC8020: "Node islands (ni/* branches)",
SC8021: "Node islands (ni/* branches)",
SC8022: "Node islands (ni/* branches)",
SC8023: "Node islands (ni/* branches)",
} as const satisfies Record<`SC${number}`, string>;
@@ -348,6 +348,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 {
@@ -67,6 +67,7 @@ export interface EarlyExecutableNativeFeatures {
stdioExtended?: boolean;
nodeIsland?: boolean;
moduleNamespace?: boolean;
moduleNamespaceDyn?: boolean;
zlib: boolean;
brotli?: boolean;
assert: boolean;
@@ -700,8 +700,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// String(NaN) is "NaN" in the number formatter; `NaN !== NaN` holds
// because f64 compares are IEEE compares).
if (expr.text === "Infinity" || expr.text === "NaN") {
const sym = lowerer.checker.getSymbolAtLocation(expr);
if (lowerer.isStdlibSymbol(sym)) {
if (lowerer.isStdlibGlobal(expr, expr.text)) {
return { kind: "numLit", value: expr.text === "NaN" ? NaN : Infinity, type: F64, loc };
}
}
@@ -729,7 +728,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc };
if (
Object.values(BYTES_ELEMENT_NAME).includes(expr.text) &&
lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr))
lowerer.isStdlibGlobal(expr, expr.text)
) {
return {
kind: "libCall",
@@ -16,6 +16,7 @@ import * as ts from "../ts7/adapter.js";
import {
BYTES_ELEMENT_NAME,
DYN,
F64,
STRING,
UNDEFINED_T,
type IrExpr,
@@ -133,6 +134,8 @@ export function jsGlobalValueByName(lowerer: Lowerer, name: string, loc: SrcLoc)
/** jsGlobalValueByName for the globals other than the interned builtin
* functions and constructors (builtinGlobalValue). */
export function jsSpecialGlobalValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
if (name === "NaN" || name === "Infinity")
return { kind: "numLit", value: name === "NaN" ? NaN : Infinity, type: F64, loc };
if (name === "String" || name === "Number" || name === "Boolean" || name === "BigInt")
return primitiveCtorClosure(lowerer, name, loc, true);
if (name === "ArrayBuffer")
@@ -16,6 +16,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";
@@ -3120,6 +3125,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));
// A module that reads builtin globals through the global object by
// computed names installs the program's lookup before its body runs.
const computedGlobalRead = computedGlobalReadOf(lowerer, sf);
@@ -3381,6 +3390,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.
@@ -336,6 +336,16 @@ function islandLoadPath(lowerer: Lowerer, path: string, loc: SrcLoc): IrExpr {
return literal;
}
/** The path literal of a module the program shares with its islands
* (lower-shared-modules.ts), named by its path in the shipped tree like a
* load's. */
export function islandSharedPath(lowerer: Lowerer, path: string, loc: SrcLoc): IrExpr {
const literal: IrExpr & { kind: "strLit" } = { kind: "strLit", value: path, type: STRING, loc };
lowerer.nodeIslandModules.addProvided(path);
lowerer.nodeIslandLoads.push({ literal, path });
return literal;
}
function loadIslandModule(lowerer: Lowerer, path: string, loc: SrcLoc): IrExpr {
return {
kind: "libCall",
@@ -680,8 +690,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;
@@ -691,8 +720,7 @@ function withClassMethods(
for (const m of c.def.methods ?? [])
if (!names.includes(m) && islandMethodName(m)) 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);
@@ -721,13 +749,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,602 @@
/* 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 { requireModuleSpecifiers, scanModuleSpecifiers } from "../../node-islands/modules.js";
import { readProjectConfig } from "../../project-config.js";
import {
sharedClassExtendedDiag,
sharedModuleDuplicatedDiag,
} from "../../diagnostics/diagnostic.js";
import {
classConstructHelper,
classMethodsBuilder,
islandSharedPath,
} from "./lower-node-island.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { funcTypeFromParamShapes } from "./call-signatures.js";
import { stripTypeWrappers } from "./typed-arguments.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;
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. */
/** Whether evaluating a module-level initializer runs no code: literals,
* names, functions and classes it creates, `require()` of a module (which
* is judged on its own), and object, array and operator expressions over
* those. */
function inertInitializer(node: ts.Expression): boolean {
const e = stripTypeWrappers(node);
if (
ts.isIdentifier(e) ||
ts.isLiteralExpression(e) ||
ts.isNoSubstitutionTemplateLiteral(e) ||
ts.isArrowFunction(e) ||
ts.isFunctionExpression(e) ||
e.kind === ts.SyntaxKind.TrueKeyword ||
e.kind === ts.SyntaxKind.FalseKeyword ||
e.kind === ts.SyntaxKind.NullKeyword
)
return true;
if (ts.isClassExpression(e))
return !e.members.nodes.some(
(m) =>
ts.isClassStaticBlockDeclaration(m) ||
(ts.isPropertyDeclaration(m) &&
m.initializer !== undefined &&
!inertInitializer(m.initializer)),
);
if (ts.isParenthesizedExpression(e)) return inertInitializer(e.expression);
if (ts.isPrefixUnaryExpression(e))
return (
e.operator !== ts.SyntaxKind.PlusPlusToken &&
e.operator !== ts.SyntaxKind.MinusMinusToken &&
inertInitializer(e.operand)
);
if (ts.isBinaryExpression(e))
return (
!ts.isAssignmentOperator(e.operatorToken.kind) &&
e.operatorToken.kind !== ts.SyntaxKind.CommaToken &&
inertInitializer(e.left) &&
inertInitializer(e.right)
);
if (ts.isConditionalExpression(e))
return (
inertInitializer(e.condition) && inertInitializer(e.whenTrue) && inertInitializer(e.whenFalse)
);
if (ts.isArrayLiteralExpression(e))
return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inertInitializer(el));
if (ts.isObjectLiteralExpression(e))
return e.properties.nodes.every(
(p) =>
ts.isShorthandPropertyAssignment(p) ||
ts.isMethodDeclaration(p) ||
(ts.isPropertyAssignment(p) &&
!ts.isComputedPropertyName(p.name) &&
inertInitializer(p.initializer)),
);
if (ts.isCallExpression(e))
return (
ts.isIdentifier(e.expression) &&
e.expression.text === "require" &&
e.arguments.nodes.length === 1 &&
ts.isStringLiteralLike(e.arguments.nodes[0]!)
);
return false;
}
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) {
// A copy would run the initializer a second time.
if (d.initializer !== undefined && !inertInitializer(d.initializer))
return "its variable initializers run code";
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. A
// target the program compiles itself is loaded in Node as well: shared.
const queue: string[] = [];
const candidates = new Set<ts.SourceFile>();
for (const sf of lowerer.moduleOrder) {
const text = sf.text;
if (!text.includes("requireModule")) continue;
const from = real(sf.fileName);
for (const spec of requireModuleSpecifiers(text)) {
try {
const target = real(resolveFrom(from, spec));
const own = compiled.get(target);
if (own !== undefined) candidates.add(own);
else queue.push(target);
} catch {
/* lowering reports it */
}
}
}
for (const island of lowerer.nodeIslands?.packages.values() ?? [])
packageJsFiles(island.dir, queue);
if (queue.length === 0 && candidates.size === 0) return plans;
const seen = new Set<string>();
// 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;
}
// Specifiers in code only: a comment or a string that looks like an
// import names no module.
for (const spec of scanModuleSpecifiers(text).specifiers) {
for (const target of resolveAll(file, spec)) {
const sf = compiled.get(target);
if (sf !== undefined) {
candidates.add(sf);
const base = extendedImport(text, spec);
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;
// Before island loads are lowered (the plans are computed in the first
// module's header): the module tree ships these as placeholders.
for (const plan of plans) lowerer.nodeIslandModules.addProvided(plan.path);
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: [
islandSharedPath(lowerer, plan.path, 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;
}
@@ -41,7 +41,11 @@ import {
type UnitEvents,
type WaveHost,
} from "./unit-journal.js";
import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js";
import {
moduleNamespaceDynValue,
moduleNamespaceObject,
moduleNamespaceValue,
} from "./module-namespace-elements.js";
import { keyedHelperName, moduleTagNumber, portableRelativePath } from "./stable-names.js";
import { positionTag, StructuralPositions } from "./structural-positions.js";
import {
@@ -201,6 +205,7 @@ import {
captureContextBinding,
declareContextLocal,
declareContextThis,
newFnCtx,
type FnCtx,
} from "./function-context.js";
import type { FrontendServices } from "../services.js";
@@ -1473,7 +1478,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
@@ -3636,6 +3644,11 @@ export class Lowerer {
}[] = [];
/** Each island module load's path literal, set to its tree path when lowering ends. */
readonly nodeIslandLoads: { literal: IrExpr & { kind: "strLit" }; path: string }[] = [];
/** 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. */
@@ -4115,6 +4128,10 @@ export class Lowerer {
setTable("freezableArrays", this.freezableArrays),
checkedTable("moduleOrder", () => this.moduleOrder.length),
checkedTable("diags", () => this.diags.length),
// The shared-module plan runs inside the first unit that lowers a
// module; a warning it adds is a diagnostic of that unit, so its count
// is journaled like `diags` (the plan itself is unjournaled).
checkedTable("sharedModuleWarnings", () => this.sharedModuleWarnings.length),
// Node island call sites hold literals of the unit's IR that lowering's
// end rewrites to module tree paths (nodeIslandRecord): a unit that adds
// one lowers again rather than replaying a record.
@@ -4390,6 +4407,11 @@ export class Lowerer {
"argumentCountSlots",
"coercions.widenedReturnTypes",
"packageFileMemo",
// The modules shared with the islands (NI-6): a pure function of the
// program's modules, computed once, and the TDZ flags its plan gives a
// statement.
"sharedModules",
"sharedInitFlags",
"slotOriginMemo",
"recordOriginMemo",
"boundaryCastOrder",
@@ -5231,6 +5253,79 @@ export class Lowerer {
return this.moduleOrder.find((sf) => tsgoPath(resolve(sf.fileName)) === path) ?? null;
}
/** A module namespace taken as a checked-dynamic value (`const u: unknown
* = ns`, `take(ns)`): the runtime's namespace object — one per module,
* Node's sorted export keys, live reads through a compiled getter, and
* Node's read-only write errors — built by `moduleNamespaceDynValue`.
* `node:` builtins have no compiled source and no complete export table,
* so they keep their fence. */
moduleNamespaceDyn(ns: IrExpr & { type: IrType & { kind: "moduleNs" } }): IrExpr | null {
if (!ns.type.moduleId.startsWith("file:")) return null;
return moduleNamespaceDynValue(this, ns);
}
/** A `Promise<module namespace>` taken as `Promise<unknown>` (or as
* `unknown`): one lifted async mapper per module awaits the promise and
* answers the same namespace object a bare namespace value boxes to, so
* the settlement is Node's namespace while the value stays a promise.
* The import() promise reaches these slots alone, and the runtime's
* per-module namespace cache keeps `await mapped === await original`; a
* rejection propagates unchanged. */
moduleNamespacePromiseDynValue(
value: IrExpr & { type: IrType & { kind: "promise" } },
): IrExpr | null {
const inner = value.type.inner;
if (inner.kind !== "moduleNs" || !inner.moduleId.startsWith("file:")) return null;
const loc = value.loc;
const name = `%module.promise:${inner.moduleId}`;
const promiseT: IrType & { kind: "promise" } = { kind: "promise", inner };
if (!this.liftedFns.some((fn) => fn.name === name)) {
const context = newFnCtx(false, null, null, DYN);
context.isAsync = true;
const previousClass = this.currentClass;
this.currentClass = null;
this.fnStack.push(context);
let namespace: IrExpr | null = null;
try {
const awaited: IrExpr & { type: IrType & { kind: "moduleNs" } } = {
kind: "awaitExpr",
value: varRef("promise", promiseT, loc),
type: inner,
loc,
};
namespace = moduleNamespaceDynValue(this, awaited);
if (namespace) {
const fn: IrFunction = {
name,
params: [{ localId: "promise", name: "promise", type: promiseT }],
returnType: DYN,
locals: [
{ id: "promise", name: "promise", type: promiseT, mutable: false },
...context.locals,
],
body: [{ kind: "return", value: namespace, loc }],
async: true,
loc,
};
this.liftedFns.push(fn);
}
} finally {
this.fnStack.pop();
this.currentClass = previousClass;
}
// A namespace with no compiled source has no dynamic value: keep the
// promise unconverted so the destination's own fence reports it.
if (!namespace) return null;
}
return {
kind: "call",
callee: name,
args: [value],
type: { kind: "promise", inner: DYN },
loc,
};
}
builtinErrorInfoOf(symbol: ts.Symbol | null | undefined): ClassInfo | null {
return builtinErrorInfoOf(this, symbol);
}
@@ -11985,6 +12080,21 @@ export class Lowerer {
const result = this.coerceToExpected(expr.result, expected);
return { ...expr, result, type: result.type };
}
// A `Promise<module namespace>` widening into `Promise<unknown>`, or
// into an `unknown` slot: map the payload through the namespace's
// dynamic value (moduleNamespacePromiseDynValue). A destination that
// wants the namespace type itself keeps the promise unchanged; the
// union path reuses this through its single-promise-arm conversion.
if (
expr.type.kind === "promise" &&
expr.type.inner.kind === "moduleNs" &&
(expected.kind === "dyn" || (expected.kind === "promise" && expected.inner.kind === "dyn"))
) {
const mapped = this.moduleNamespacePromiseDynValue(
expr as IrExpr & { type: IrType & { kind: "promise" } },
);
if (mapped) expr = mapped;
}
// An instance of a class declared `extends Array` is its array (the
// same object); an array slot of another element type converts it as
// an array.
@@ -12132,6 +12242,16 @@ export class Lowerer {
const object = moduleNamespaceObject(this, ns) ?? moduleNamespaceValue(this, ns);
if (object) return object;
}
// A TypeScript source's module namespace as an untyped value: the
// runtime's namespace object with live reads, Node's sorted keys and
// read-only writes. `node:` builtins have no compiled source or
// complete export table here and keep their fence below.
if (expr.type.kind === "moduleNs") {
const boxed = this.moduleNamespaceDyn(
expr as IrExpr & { type: IrType & { kind: "moduleNs" } },
);
if (boxed) return boxed;
}
if (
expr.type.kind === "moduleNs" &&
expr.type.moduleId === "builtin:events" &&
@@ -4,6 +4,7 @@ import * as ts from "../ts7/adapter.js";
import {
DYN,
STRING,
SYMBOL_T,
type IrExpr,
type IrFunction,
type IrStmt,
@@ -173,28 +174,28 @@ export function moduleNamespaceObject(
};
}
/** A module namespace taken as an untyped value whose exports a copy
* cannot represent: a binding that can change after initialization (`let`,
* `var`), or an export the copy cannot name. The value is the runtime's
* namespace object (scr_module_namespace_value): one per module, whose
* property reads call a compiled getter for the export's current value and
* whose writes throw Node's TypeError. An export whose value has no
* checked-dynamic form throws its fence when it is read, and listing the
* object's keys or inspecting it throws the "not supported yet" error. */
export function moduleNamespaceValue(
/** The compiled getter behind a namespace object: one lifted function per
* module that answers an export's current value by name, plus the module's
* sorted export names. An export whose value has no checked-dynamic form
* throws its fence when it is read. */
function moduleNamespaceGetter(
lowerer: Lowerer,
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
): IrExpr | null {
): { name: string; keys: string[] } | null {
const source = lowerer.sourceFileOfModuleNamespace(ns.type);
if (!source) return null;
const symbol = lowerer.checker.getSymbolAtLocation(source);
if (!symbol) return null;
const exports: { key: string; declaration: ts.Node | undefined }[] = [];
const exports: { key: string; declaration: ts.Node | undefined; symbol: ts.Symbol }[] = [];
for (const exported of lowerer.checker.getExportsOfModule(symbol)) {
const target =
exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
if (!(target.flags & ts.SymbolFlags.Value)) continue;
exports.push({ key: ts.symbolName(exported), declaration: target.valueDeclaration });
exports.push({
key: ts.symbolName(exported),
declaration: target.valueDeclaration,
symbol: target,
});
}
exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
const name = `%module.namespace:${ns.type.moduleId}`;
@@ -206,24 +207,37 @@ export function moduleNamespaceValue(
lowerer.fnStack.push(context);
try {
const body: IrStmt[] = [];
for (const { key, declaration } of exports) {
for (const { key, declaration, symbol: exportSymbol } of exports) {
const node = declaration ?? source;
const diagsBefore = lowerer.diags.length;
let read: IrStmt[];
try {
const identifier = declaration?.name as ts.Node | undefined;
if (!identifier || !ts.isIdentifier(identifier)) {
lowerer.unsupported(
"SC1090",
node,
`reading the unnamed export '${key}' through a module namespace object`,
);
let value: IrExpr;
if (identifier && ts.isIdentifier(identifier)) {
value = lowerer.lowerExprExpecting(identifier, DYN);
} else {
// `export default <expression>` and the anonymous default
// class/function have no named binding to read. The module's
// pre-registered default global holds the value the export
// statement snapshotted — the same storage a default import
// reads — so the namespace property answers it instead of
// fencing. A default whose type has no checked-dynamic form
// still fails the coercion below and keeps its fence.
const storage = lowerer.globalsBySymbol.get(exportSymbol);
if (!storage) {
lowerer.unsupported(
"SC1090",
node,
`reading the unnamed export '${key}' through a module namespace object`,
);
}
value = lowerer.coerceToExpected(varRef(storage.id, storage.type, loc), DYN);
}
const value = lowerer.lowerExprExpecting(identifier, DYN);
if (value.type.kind !== "dyn") {
lowerer.unsupported(
"SC1101",
identifier,
identifier ?? node,
`reading the export '${key}' ('${lowerer.fmt(value.type)}') through a module namespace object`,
);
}
@@ -263,6 +277,24 @@ export function moduleNamespaceValue(
lowerer.currentClass = previousClass;
}
}
return { name, keys: exports.map(({ key }) => key) };
}
/** A module namespace taken as an untyped value whose exports a copy
* cannot represent: a binding that can change after initialization (`let`,
* `var`), or an export the copy cannot name. The value is the runtime's
* namespace object (scr_module_namespace_value): one per module, whose
* property reads call a compiled getter for the export's current value and
* whose writes throw Node's TypeError. An export whose value has no
* checked-dynamic form throws its fence when it is read, and listing the
* object's keys or inspecting it throws the "not supported yet" error. */
export function moduleNamespaceValue(
lowerer: Lowerer,
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
): IrExpr | null {
const getter = moduleNamespaceGetter(lowerer, ns);
if (!getter) return null;
const loc = ns.loc;
const value: IrExpr = {
kind: "libCall",
fn: "module.namespaceValue",
@@ -270,13 +302,82 @@ export function moduleNamespaceValue(
strLit(ns.type.moduleId, loc),
{
kind: "arrayLit",
elems: exports.map(({ key }) => strLit(key, loc)),
elems: getter.keys.map((key) => strLit(key, loc)),
type: arrayOf(STRING),
loc,
},
{
kind: "dynFrom",
value: { kind: "closure", fnName: name, captures: [], type: funcOf([STRING], DYN), loc },
value: {
kind: "closure",
fnName: getter.name,
captures: [],
type: funcOf([STRING], DYN),
loc,
},
type: DYN,
loc,
},
],
type: DYN,
loc,
};
if (ns.kind === "moduleNsRef") return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: ns, loc }],
result: value,
type: DYN,
loc,
};
}
/** A module namespace boxed for typed code through a dynamic slot
* (`const u: unknown = ns`, `take(ns)`): Node's namespace object as
* scr_module_namespace_dyn.c builds it — one non-extensible null-prototype
* object per module whose own sorted export names are enumerable read-only
* accessors, whose `Symbol.toStringTag` is "Module", whose reads call the same
* compiled getter, and whose writes and coerces throw Node's TypeErrors. A
* `node:` builtin has no compiled export table, so it keeps its fence. */
export function moduleNamespaceDynValue(
lowerer: Lowerer,
ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
): IrExpr | null {
const getter = moduleNamespaceGetter(lowerer, ns);
if (!getter) return null;
const loc = ns.loc;
const value: IrExpr = {
kind: "libCall",
fn: "module.namespaceDyn",
args: [
strLit(ns.type.moduleId, loc),
{
kind: "arrayLit",
elems: getter.keys.map((key) => strLit(key, loc)),
type: arrayOf(STRING),
loc,
},
{
kind: "dynFrom",
value: {
kind: "closure",
fnName: getter.name,
captures: [],
type: funcOf([STRING], DYN),
loc,
},
type: DYN,
loc,
},
{
kind: "dynFrom",
value: {
kind: "libCall",
fn: "sym.wellKnown",
args: [strLit("toStringTag", loc)],
type: SYMBOL_T,
loc,
},
type: DYN,
loc,
},
@@ -7,8 +7,10 @@
* (`%dyn.class.call.N`) that calls such a method already holds a capsule
* of the same instance at its exact class: its receiver after
* normalization. The helper now calls a copy of the method that returns
* nothing and answers its own receiver, so the call boxes nothing. Other
* callers (vtables, typed code, method values) keep the method itself. */
* nothing and answers its own receiver, so the call boxes nothing. A
* direct call whose result nothing reads calls the copy too. Other callers
* (vtables, method values, calls that use the result) keep the method
* itself. */
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { VOID } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
@@ -57,6 +59,20 @@ export function returnDispatchReceivers(lowerer: Lowerer, functions: IrFunction[
];
}
}
// A direct call whose result nothing reads (`builder.set(1);` in typed
// code) needs no capsule at all.
for (const fn of [...functions]) {
everyStmtList(fn.body, {
stmt: (s) => {
if (s.kind === "exprStmt" && s.expr.kind === "call") {
const copy = copyOf(s.expr.callee);
if (copy) s.expr = { ...s.expr, callee: copy.name, type: VOID };
}
return true;
},
expr: () => true,
});
}
}
/** The helper's receiver is assigned only by its normalization, so it is
+1
View File
@@ -702,6 +702,7 @@ async function compileExecutableNative(
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -23,6 +23,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"nodeIsland.construct",
"nodeIsland.instanceof",
"nodeIsland.classMethods",
"nodeIsland.classMethodsOf",
"nodeIsland.copyArrays",
// Node's argument, cipher-name, IV, tag-length and key-length errors.
"crypto.cipherNew",
@@ -207,6 +207,14 @@ function buildLibFnSignatures() {
/** 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 },
/** Island arrays of copied values as copies (a result, or an awaited
* promise's value). */
"nodeIsland.copyArrays": { argTypes: [DYN], result: DYN },
@@ -3604,6 +3612,11 @@ function buildLibFnSignatures() {
* (scr_module_namespace_value): the module id, its sorted export names
* and the compiled getter. One object per module. Never throws. */
"module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN },
/** A module namespace as typed code sees it through a dynamic slot
* (scr_module_namespace_dyn_value): the module id, its sorted export
* names, the compiled getter, and the Symbol.toStringTag key. Draws
* Node's frozen null-prototype object, one per module. Never throws. */
"module.namespaceDyn": { argTypes: [STRING, arrayOf(STRING), DYN, DYN], result: DYN },
"fs.callbackValue": { argTypes: [STRING], result: DYN },
"fs.callbackCall": { argTypes: [STRING, DYN], result: DYN },
"process.hrtimeValue": { argTypes: [], result: DYN },
+3 -1
View File
@@ -1037,7 +1037,9 @@ export function isRefCounted(t: IrType): boolean {
* Only the relative paths enter an executable. */
export interface IrNodeIslandModules {
root: string;
files: { path: string; size: number; sha256: string }[];
/** `placeholder`: a program module the islands share, shipped as
* providedModulePlaceholder (node-islands/modules.ts), not its source. */
files: { path: string; size: number; sha256: string; placeholder?: true }[];
links: { path: string; target: string }[];
/** Where the executable finds the tree: absent when it carries the tree
* itself, else a `$ORIGIN/` path (`--libnode=beside`). */
@@ -1197,6 +1197,8 @@ const RUNTIME_FEATURE_RULES = {
/** Module namespace objects as checked-dynamic values
* (scr_module_namespace.c). */
moduleNamespace: rule({ fns: ["module.namespaceValue"] }),
/** A module namespace boxed for typed code (scr_module_namespace_dyn.c). */
moduleNamespaceDyn: rule({ fns: ["module.namespaceDyn"] }),
/** The WebAssembly API (scr_wasm.c). */
wasm: rule({ prefixes: ["wasm."] }),
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
@@ -1296,6 +1298,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
urlIdna: false,
stdioExtended: false,
moduleNamespace: false,
moduleNamespaceDyn: false,
stream: false,
streamDyn: false,
handleProps: false,
@@ -34,6 +34,7 @@ const features = {
nodeIsland: false,
brotli: false,
moduleNamespace: false,
moduleNamespaceDyn: false,
regex: true,
copying: false,
textDecoderLegacy: false,
@@ -41,7 +41,7 @@ import {
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/";
@@ -6,6 +6,7 @@ import {
NodeIslandModuleCollector,
nodeIslandManifest,
readNodeIslandModules,
requireModuleSpecifiers,
scanModuleSpecifiers,
} from "./modules.js";
@@ -259,3 +260,53 @@ describe("the collected module tree", () => {
expect(readNodeIslandModules(tree)).toBe(`${join(root, "m.cjs")} changed during the build`);
});
});
describe("modules the program provides", () => {
test("a shared program module ships as a placeholder, not its source", () => {
const source = "// internal pricing rules\nexport const margin = 0.37;\n";
const root = project({
"src/main.ts": "",
"src/pricing.ts": source,
"src/plugin.mjs": "export const x = 1;",
});
const collector = new NodeIslandModuleCollector();
expect(collector.add(join(root, "src/main.ts"), "./plugin.mjs")).toBeNull();
collector.addProvided(join(root, "src/pricing.ts"));
const tree = collector.tree()!;
const entry = tree.files.find((f) => f.path.endsWith("pricing.ts"))!;
expect(entry.placeholder).toBe(true);
const data = readNodeIslandModules(tree);
if (typeof data === "string") throw new Error(data);
const shipped = new TextDecoder().decode(data);
expect(shipped).not.toContain("internal pricing rules");
expect(shipped).not.toContain("margin");
expect(shipped).toContain("export const x = 1;");
});
test("island code that imports a provided module ships neither its source nor its imports", () => {
const root = project({
"src/main.ts": "",
"src/plugin.mjs": 'import { y } from "./shared.mjs";\nexport const z = y;',
"src/shared.mjs": 'import { secret } from "./internal.mjs";\nexport const y = secret;',
"src/internal.mjs": "export const secret = 42;",
});
const collector = new NodeIslandModuleCollector();
collector.addProvided(join(root, "src/shared.mjs"));
expect(collector.add(join(root, "src/main.ts"), "./plugin.mjs")).toBeNull();
const files = collector.tree()!.files;
expect(files.find((f) => f.path.endsWith("shared.mjs"))!.placeholder).toBe(true);
expect(files.some((f) => f.path.endsWith("internal.mjs"))).toBe(false);
});
test("requireModule specifiers come from code, not comments or strings", () => {
expect(
requireModuleSpecifiers(`
// requireModule("./commented.mjs")
/* requireModule("./block.mjs") */
const s = 'requireModule("./in-string.mjs")';
const t = \`requireModule("./in-template.mjs")\`;
const p = requireModule("./plugin.mjs");
`),
).toEqual(["./plugin.mjs"]);
});
});
+68 -11
View File
@@ -172,8 +172,10 @@ export class NodeIslandModuleCollector {
private readonly traced = new Set<string>();
/** Real paths of the calling modules. */
private readonly callers = new Set<string>();
/** Real paths of the files loads name (addLoaded). */
/** Real paths of the files loads name (addLoaded, addProvided). */
private readonly loaded = new Set<string>();
/** Program modules whose source the program provides (addProvided). */
private readonly provided = new Set<string>();
/** Collects the module `specifier` names from `caller` (a real path).
* Returns null, or the reason the build cannot ship it. */
@@ -195,6 +197,23 @@ export class NodeIslandModuleCollector {
return this.addModule(this.placed(file));
}
/** Collects a module whose source the program provides (a shared module,
* runtime scr_node_island_hooks.cjs): Node still resolves it to its file,
* so its package is in the tree, or, for a program module, a placeholder
* at its path (providedModulePlaceholder): the program's source stays out
* of the executable. Island code that imports it loads the program's
* module too, so it is not traced either. */
addProvided(file: string): void {
this.loaded.add(file);
const at = packageDirOf(file);
if (at !== null) this.addPackage(at.dir);
else {
const placed = this.placed(file);
this.files.add(placed);
this.provided.add(placed);
}
}
/** The collected tree, or null when no call loads a file. */
tree(): NodeIslandModuleTree | null {
if (this.files.size === 0 && this.links.size === 0) return null;
@@ -213,13 +232,15 @@ export class NodeIslandModuleCollector {
if (outer >= 0) root = parts.slice(0, outer).join(sep) || sep;
const files: NodeIslandModuleFile[] = [];
for (const file of [...this.files].sort()) {
const path = portable(relative(root, file));
if (this.provided.has(file)) {
const bytes = providedModulePlaceholder(path);
files.push({ path, size: bytes.length, sha256: sha256(bytes), placeholder: true });
continue;
}
const bytes = trackedReadBytes(file);
if (bytes === null) continue;
files.push({
path: portable(relative(root, file)),
size: bytes.length,
sha256: sha256(bytes),
});
files.push({ path, size: bytes.length, sha256: sha256(bytes) });
}
const links: NodeIslandModuleLink[] = [...this.links]
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
@@ -295,6 +316,9 @@ export class NodeIslandModuleCollector {
if (this.traced.has(file)) return null;
this.traced.add(file);
this.files.add(file);
// The program provides its source: island code never runs this file,
// so what it imports is not shipped for it.
if (this.provided.has(file)) return null;
const scope = packageScope(dirname(file));
if (scope !== null) this.files.add(scope);
if (!TRACED.test(file)) return null;
@@ -566,6 +590,27 @@ export interface ModuleSpecifierScan {
computed: { at: number; form: string }[];
}
/** The string-literal arguments of `requireModule(...)` calls in source
* code, comments and template text excluded. */
export function requireModuleSpecifiers(text: string): string[] {
const tokens = moduleTokens(text);
const out: string[] = [];
for (let k = 0; k + 3 < tokens.length; k++) {
const [name, open, arg, close] = [tokens[k]!, tokens[k + 1]!, tokens[k + 2]!, tokens[k + 3]!];
if (
name.kind === "name" &&
name.value === "requireModule" &&
open.kind === "punct" &&
open.value === "(" &&
arg.kind === "string" &&
close.kind === "punct" &&
close.value === ")"
)
out.push(arg.value);
}
return out;
}
export function scanModuleSpecifiers(text: string): ModuleSpecifierScan {
const tokens = moduleTokens(text);
const specifiers: string[] = [];
@@ -678,6 +723,16 @@ export function scanModuleSpecifiers(text: string): ModuleSpecifierScan {
/* ── shipping the tree ────────────────────────────────────────────────── */
/** What the tree ships at the path of a program module the islands share:
* Node resolves the module to this file and the program's loader hooks
* answer its source (runtime scr_node_island_hooks.cjs), so the file holds
* none of the program's source. Evaluating it throws. */
export function providedModulePlaceholder(path: string): Uint8Array {
return new TextEncoder().encode(
`throw new Error(${JSON.stringify(`${path} is provided by the compiled program`)});\n`,
);
}
/** The manifest the executable records (scr_node_island.c reads it):
* `{"files":[[path,size,sha256],...],"links":[[path,target],...]}`. Its
* SHA-256 names the tree. */
@@ -696,11 +751,13 @@ export function readNodeIslandModules(modules: IrNodeIslandModules): Uint8Array
for (const file of modules.files) {
const path = join(modules.root, ...file.path.split("/"));
let bytes: Uint8Array;
try {
bytes = new Uint8Array(readFileSync(path));
} catch {
return `${path} was removed during the build`;
}
if (file.placeholder === true) bytes = providedModulePlaceholder(file.path);
else
try {
bytes = new Uint8Array(readFileSync(path));
} catch {
return `${path} was removed during the build`;
}
if (bytes.length !== file.size || sha256(bytes) !== file.sha256)
return `${path} changed during the build`;
parts.push(bytes);
@@ -312,7 +312,7 @@ globalThis.__scriptcNodeIsland = {
},
setTimer: noop, clearTimer: noop, refTimer: noop, refreshTimer: noop,
setImmediate: noop, clearImmediate: noop, refImmediate: noop,
nextTick: noop, emit: noop, load: noop,
nextTick: noop, emit: noop, load: noop, sharedPush: noop,
};
const fnToString = Function.prototype.toString;
const hasOwn = Object.prototype.hasOwnProperty;
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/6850-node-island-shared-singleton/state.mjs",
"<repo>/tests/corpus/6850-node-island-shared-singleton/tally.mjs",
"<repo>/tests/corpus/6850-node-island-shared-singleton/main.mjs"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6851-node-island-shared-class/shapes.mjs",
"<repo>/tests/corpus/6851-node-island-shared-class/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6852-node-island-shared-cycle/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6853-node-island-shared-registry/main.mjs"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6854-node-island-shared-registry-native/registry.mjs",
"<repo>/tests/corpus/6854-node-island-shared-registry-native/main.mjs"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/8412-module-namespace-untyped/mod.ts",
"<repo>/tests/corpus/8412-module-namespace-untyped/mod-default.ts",
"<repo>/tests/corpus/8412-module-namespace-untyped/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dynamic-method-receiver-statements.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/shorthand-builtin-values.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/shorthand-builtin-values.ts"
],
"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));
@@ -267,6 +271,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);
SetMethod(context, native, "isExport", IsExport);
global->Set(context, Utf8(isolate, "__scriptcNodeIslandHost"), native).Check();
TryCatch try_catch(isolate);
@@ -300,7 +307,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);
@@ -638,7 +646,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;
@@ -909,6 +917,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;
@@ -920,6 +936,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>());
@@ -1103,6 +1128,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();
@@ -1563,6 +1596,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,
@@ -1599,6 +1682,7 @@ const ScrNodeBridge kBridge = {
DropHandle,
LiveHandles,
ExportSettle,
SharedPush,
};
} // namespace
+1 -1
View File
@@ -46,7 +46,7 @@ static const char kBootstrap[] =
" },\n"
" setTimer() { return 0; }, clearTimer() {}, refTimer() {}, refreshTimer() {},\n"
" setImmediate() { return 0; }, clearImmediate() {}, refImmediate() {},\n"
" nextTick() {}, emit() {}, load() {},\n"
" nextTick() {}, emit() {}, load() {}, sharedPush() {},\n"
"};\n";
static double now_ms(void) {
@@ -93,6 +93,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_perf_loop.c", "perfLoop"],
// Module namespace objects as checked-dynamic values.
["scr_module_namespace.c", "moduleNamespace"],
// The same namespace as the frozen null-prototype object typed code sees.
["scr_module_namespace_dyn.c", "moduleNamespaceDyn"],
// Blob and File box their reads as dyn promises through scr_async_dyn.c.
["scr_blob.c", "dynAsync"],
["scr_zlib.c", "zlib"],
+7
View File
@@ -34,6 +34,13 @@ ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member);
* one handle per module `id`, whose reads call `getter` with the export
* name when `keys` lists it. Borrows all three; +1 handle. */
ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter);
/* A module namespace as checked-dynamic code sees it
* (scr_module_namespace_dyn.c): one non-extensible null-prototype object per
* module `id`, Node's sorted export names as enumerable read-only accessors
* whose getters call `getter` with the name, `Symbol.toStringTag` "Module"
* (the well-known symbol is `tag_key`), and Node's strict-mode write
* TypeErrors. Borrows all four; +1 object. */
ScrDyn *scr_module_namespace_dyn_value(ScrStr *id, ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key);
ScrDyn *scr_global_native(ScrArr *known);
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void));
ScrDyn *scr_console_native(void);
+12
View File
@@ -2652,6 +2652,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);
@@ -0,0 +1,143 @@
/* Module namespace objects handed to typed code as checked-dynamic values.
*
* In Node a module namespace is a non-extensible null-prototype object whose
* own properties are the module's exports in sorted order, which are
* enumerable and read-only, whose Symbol.toStringTag is "Module", and whose
* ToPrimitive finds no method. Every generic dynamic operation —
* Object.keys/values/entries/getOwnPropertyNames, for-in, `in`,
* JSON.stringify, identity, member reads and calls — already handles that
* shape through scr_json.c's object
* dispatch, so this unit only builds the object and links only into programs
* that box a namespace.
*
* The exports stay live: each property is an accessor whose getter calls the
* compiled namespace getter with the export name (the same getter the
* JavaScript-source representation uses), and whose setter throws Node's
* strict-mode read-only TypeError. An unknown name is not a member, so the
* object's own not-extensible state answers Node's "Cannot add property".
* One object per module keeps its identity.
*
* Node models the exports as writable data properties on a sealed object, so
* Object.isFrozen answers false and Object.isSealed true. Live reads need
* accessors here, and the accessor shape cannot be sealed without also
* freezing: Object.isFrozen and Object.isExtensible answer as Node's do,
* Object.isSealed does not. */
#include "scr_runtime.h"
#include <stdlib.h>
#include <string.h>
typedef struct ScrModuleNamespaceDyn {
ScrStr *id;
ScrDyn *object; /* retained for the program's lifetime */
struct ScrModuleNamespaceDyn *next;
} ScrModuleNamespaceDyn;
static SCR_TL ScrModuleNamespaceDyn *scr_module_namespace_dyns;
static SCR_TL bool scr_module_namespace_dyn_cleanup_registered;
static void scr_module_namespace_dyn_cleanup(void) {
while (scr_module_namespace_dyns) {
ScrModuleNamespaceDyn *ns = scr_module_namespace_dyns;
scr_module_namespace_dyns = ns->next;
scr_str_release(ns->id);
scr_dyn_release(ns->object);
free(ns);
}
}
/* An export's current value: the compiled getter takes the export name as a
* dyn string and answers it +1. */
static ScrDyn *scr_module_namespace_dyn_read(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrDyn *getter = scr_box_get_ref(clo->caps[0]);
ScrDyn *name = scr_box_get_ref(clo->caps[1]);
ScrDyn *argument = scr_dyn_retain(name);
ScrDyn *value = scr_dyn_call(getter, &argument, 1, "module namespace export");
scr_dyn_release(argument);
scr_dyn_release(name);
scr_dyn_release(getter);
return value;
}
/* A namespace export is read-only: Node's strict-mode TypeError names the
* export and the namespace's [object Module] tag. */
static ScrDyn *scr_module_namespace_dyn_write(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrDyn *name = scr_box_get_ref(clo->caps[0]);
ScrJsonBuf text;
scr_jb_init(&text);
scr_jb_puts(&text, "Cannot assign to read only property '");
scr_jb_write(&text, name->v.str->data, name->v.str->len);
scr_jb_puts(&text, "' of object '[object Module]'");
ScrStr *message = scr_jb_finish(&text);
scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len);
scr_str_release(message);
scr_dyn_release(name);
return NULL;
}
/* `thunk` as a dynamic function over one or two captured dynamic values. */
static ScrDyn *scr_module_namespace_dyn_fn(ScrDynThunk thunk, ScrDyn *first, ScrDyn *second) {
ScrClosure *closure = scr_closure_new(NULL, second ? 2 : 1);
closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(closure->caps[0], scr_dyn_retain(first));
if (second) {
closure->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(closure->caps[1], scr_dyn_retain(second));
}
return scr_dyn_new_func(closure, thunk, 0, "native:module namespace", "module namespace export");
}
/* The namespace object: null prototype, one enumerable read-only accessor per
* export in `keys` order, a non-writable Symbol.toStringTag of "Module", and
* not extensible. */
static ScrDyn *scr_module_namespace_dyn_build(ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key) {
ScrDyn *object = scr_dyn_new_obj_null_proto();
for (size_t i = 0; i < keys->len; i++) {
ScrStr *key = scr_arr_get_ref(keys, (double)i); /* +1 */
ScrDyn *name = scr_dyn_new_str(key);
ScrDyn *read = scr_module_namespace_dyn_fn(scr_module_namespace_dyn_read, getter, name);
ScrDyn *write = scr_module_namespace_dyn_fn(scr_module_namespace_dyn_write, name, NULL);
scr_dyn_obj_set_accessor(object, key->data, key->len, read, write);
scr_dyn_release(name);
scr_str_release(key);
}
ScrStr *tag = scr_str_new("Module", 6);
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_new_str(tag));
scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(false));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false));
scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(false));
scr_dyn_release(scr_dyn_define_property(object, tag_key, descriptor));
scr_str_release(tag);
scr_dyn_release(descriptor);
scr_dyn_release(scr_dyn_prevent_extensions(object));
if (scr_exc_pending()) {
/* The tag key is a well-known symbol, so this cannot fail in practice; a
* pending exception still must not be cached. */
scr_dyn_release(object);
return NULL;
}
return object;
}
ScrDyn *scr_module_namespace_dyn_value(ScrStr *id, ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key) {
for (ScrModuleNamespaceDyn *ns = scr_module_namespace_dyns; ns; ns = ns->next) {
if (ns->id->len == id->len && memcmp(ns->id->data, id->data, id->len) == 0)
return scr_dyn_retain(ns->object);
}
if (!scr_module_namespace_dyn_cleanup_registered) {
scr_atexit(scr_module_namespace_dyn_cleanup);
scr_module_namespace_dyn_cleanup_registered = true;
}
ScrDyn *object = scr_module_namespace_dyn_build(keys, getter, tag_key);
if (!object) return NULL;
ScrModuleNamespaceDyn *ns = malloc(sizeof(*ns));
if (!ns) scr_trap("scriptc: out of memory\n");
*ns = (ScrModuleNamespaceDyn){ scr_str_retain(id), scr_dyn_retain(object), scr_module_namespace_dyns };
scr_module_namespace_dyns = ns;
return object;
}
+22 -1
View File
@@ -17,7 +17,7 @@
#include <stddef.h>
#include <stdint.h>
#define SCR_NODE_BRIDGE_ABI 6u
#define SCR_NODE_BRIDGE_ABI 7u
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
@@ -200,6 +200,24 @@ typedef struct ScrNodeBridgeHost {
void (*export_watch)(uint32_t id);
/* V8 collected the representative of `id`. */
void (*export_release)(uint32_t id);
/* `value instanceof <export id>` for a native class representative
* (its Symbol.hasInstance): 1 or 0. */
int (*export_instance_of)(uint32_t id, const ScrNodeValue *value);
/* ── shared modules (NI-6): modules the program compiled that island
* code imports resolve, through the loader hooks of the bootstrap, to
* generated modules over the live native exports. ── */
/* The table, as SCR_NV_STRING JSON: [{"path","exports":[{"name","kind"}]}]
* (index = module id, export index = slot). */
void (*shared_table)(ScrNodeValue *out);
/* The current value of a shared export. mode 0: read; 1: read and bind
* (the generated module holds it: later native changes are pushed with
* bridge->shared_push); 2: bind late (the binding is in its temporal
* dead zone: it is pushed once the native declaration ran). 0: *out is
* the value; 1: threw (*out thrown); 3: in its temporal dead zone. */
int (*shared_get)(uint32_t mod, uint32_t slot, int mode, ScrNodeValue *out);
/* The exports in their temporal dead zone now, as SCR_NV_STRING JSON
* [[mod, slot], ...]. */
void (*shared_tdz)(ScrNodeValue *out);
} ScrNodeBridgeHost;
typedef struct ScrNodeBridge {
@@ -290,6 +308,9 @@ typedef struct ScrNodeBridge {
uint32_t (*live_handles)(void);
/* Settles the V8 promise representing native promise `id` (NI-5). */
void (*export_settle)(uint32_t id, int rejected, const ScrNodeValue *value);
/* NI-6: the native value of shared export (mod, slot) changed: the
* generated module's binding takes `value`. */
void (*shared_push)(uint32_t mod, uint32_t slot, const ScrNodeValue *value);
} ScrNodeBridge;
#endif /* SCR_NODE_BRIDGE_H */
+8
View File
@@ -987,6 +987,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();
@@ -1131,6 +1134,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;
}
@@ -1174,6 +1178,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,
};
typedef struct {
+11
View File
@@ -140,7 +140,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));
+370 -11
View File
@@ -130,6 +130,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 scr_promise_identity_key(v->v.promise);
@@ -143,7 +145,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:
@@ -301,6 +303,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);
@@ -308,6 +311,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);
@@ -317,13 +322,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);
@@ -340,6 +357,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;
@@ -368,11 +386,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. An own property the
* island wrote under the method's name (kept in the capsule's materialized
* view) hides the method, as an own property hides a prototype's in Node. */
@@ -401,8 +476,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);
@@ -432,6 +510,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 */
@@ -447,6 +526,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;
@@ -481,7 +565,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;
@@ -509,6 +595,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 {
@@ -579,8 +678,252 @@ 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);
/* Each module's file as Node resolves it: in the shipped module tree. */
char **paths = calloc(ni_nshared > 0 ? ni_nshared : 1, sizeof *paths);
if (paths == NULL) ni_export_oom();
size_t len = 2;
for (uint32_t i = 0; i < ni_nshared; i++) {
paths[i] = scr_ni_module_path(ni_shared[i].path);
len += strlen(paths[i]) * 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 (const char *p = paths[i]; *p != '\0'; p++) {
char c = *p;
if (c == '"' || c == '\\') buf[at++] = '\\';
buf[at++] = c;
}
free(paths[i]);
at += (size_t)sprintf(buf + at, "\",\"exports\":");
memcpy(buf + at, m->exports->data, m->exports->len);
at += m->exports->len;
buf[at++] = '}';
}
buf[at++] = ']';
free(paths);
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++) {
@@ -595,8 +938,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
+40 -1
View File
@@ -572,7 +572,10 @@ static bool nh_export_value(ScrDyn *v, ScrNodeValue *out, NhKeep *keep) {
scr_ni_export_out(v, out);
return true;
}
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_ARR) {
/* Arrays (dyn, and typed array capsules: NI-6 shortcut, as for dyn
* arrays) keep NI-4's JSON copy. */
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_ARR ||
(v->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(v) != NULL)) {
ScrStr *gap = scr_str_new("", 0);
ScrDyn *json = scr_json_stringify_replacer(v, scr_dyn_undefined(), gap);
scr_str_release(gap);
@@ -752,6 +755,7 @@ static void nh_call_job(void *p) {
}
static int nh_run(NhCall *c) {
scr_ni_shared_refresh();
scr_ni_on_node_stack(nh_call_job, c);
return c->threw;
}
@@ -1133,6 +1137,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());
@@ -1216,6 +1234,21 @@ static bool nh_op_set(void *h, const char *key, size_t key_len, const ScrDyn *va
return true; /* handled; a throw stays pending */
}
/* A member that reads back as one of the program's own functions (a
* module the program shares with its islands gives them its native
* exports): called natively, with the island object as the receiver. */
static bool nh_is_native_function(const ScrDyn *fn) {
return !scr_ni_is_handle(fn) && scr_dyn_is_callable(fn);
}
static ScrDyn *nh_call_native(ScrDyn *fn, ScrDyn *this_, ScrDyn *const *args, size_t argc,
const char *what) {
scr_dyn_this_push_dyn(this_);
ScrDyn *result = scr_dyn_call(fn, args, argc, what);
scr_dyn_this_pop();
return result;
}
/* obj.m(...) from dynamic code: the member read, then the call with obj
* as the receiver; a member that is not a function throws Node's TypeError
* with the call site's text (`what`), as V8 words it. */
@@ -1224,6 +1257,11 @@ static ScrDyn *nh_op_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *c
(void)h;
ScrDyn *fn = scr_ni_get_str(self, method, strlen(method));
if (fn == NULL) return NULL;
if (nh_is_native_function(fn)) {
ScrDyn *result = nh_call_native(fn, self, args, argc, what != NULL ? what : method);
scr_dyn_release(fn);
return result;
}
if (!scr_ni_is_function(fn)) {
scr_dyn_release(fn);
char message[512];
@@ -1327,6 +1365,7 @@ ScrDyn *scr_node_island_apply(ScrDyn *fn, ScrDyn *args) {
ScrDyn *scr_node_island_apply_to(ScrDyn *fn, ScrDyn *this_, ScrDyn *args) {
ScrDyn *const *items;
size_t argc = ni_libcall_args(args, &items);
if (nh_is_native_function(fn)) return nh_call_native(fn, this_, items, argc, "value");
return scr_ni_call(fn, this_, items, argc);
}
@@ -0,0 +1,149 @@
// ── 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, var or function declaration,
// whose binding JavaScript allows reassigning, 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" || e.kind === "function") {
// A function declaration's binding is reassignable, like `var`:
// the island must see the current function, not the first one.
lines.push(`let ${local} = __s.bind(${mod}, ${slot}, (v) => { ${local} = v; });`);
lines.push(`export { ${local} as ${name} };`);
} else {
lines.push(`const ${local} = __s.get(${mod}, ${slot});`);
lines.push(`export { ${local} as ${name} };`);
}
});
return lines.join("\n");
}
function entrySource(url) {
const e = entries.get(url);
const lines = [`import ${JSON.stringify(e.target)};`];
lines.push('const __s = globalThis[Symbol.for("scriptc.shared")];');
for (const [mod, slot] of e.late) {
const n = lateName(mod, slot);
lines.push(`export let ${n} = __s.late(${mod}, ${slot}, (v) => { ${n} = v; });`);
}
return lines.join("\n");
}
registerHooks({
resolve(specifier, context, nextResolve) {
const asUrl = specifier.startsWith("/") ? pathToFileURL(specifier).href : specifier;
if (entries.has(asUrl)) return { url: asUrl, shortCircuit: true };
const resolved = nextResolve(specifier, context);
// A shared module keeps its own file URL (require() and import key
// their module records alike only for file URLs); load answers it.
if (resolved.url.startsWith("file:") && byUrl.has(resolved.url))
return { url: resolved.url, format: "module", shortCircuit: true };
return resolved;
},
load(url, context, nextLoad) {
const mod = byUrl.get(url);
if (mod !== undefined)
return { format: "module", source: sharedSource(mod), shortCircuit: true };
if (entries.has(url))
return { format: "module", source: entrySource(url), shortCircuit: true };
return nextLoad(url, context);
},
});
return {
/** The entry to load the island module at `path` through, or null when
* no shared binding is in its temporal dead zone. Run `load` with it. */
entry(path, load) {
const late = JSON.parse(nativeHost.sharedTdz());
if (late.length === 0) return load(null);
// A file URL beside the island module (never on disk: the load hook
// answers it), so require() and import key it alike.
const url = pathToFileURL(join(dirname(path), `__scriptc_entry_${++entrySeq}.mjs`)).href;
entries.set(url, { target: pathToFileURL(path).href, late });
const outer = current;
current = url;
try {
return load(fileURLToPath(url));
} finally {
current = outer;
}
},
push(mod, slot, value) {
for (const set of setters.get(`${mod}:${slot}`) ?? []) set(value);
},
};
}
+15 -1
View File
@@ -222,11 +222,24 @@ 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) {
// A built-in module's name, with or without the node: prefix.
if (isBuiltin(path)) 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) ─────────────────────────────────────
@@ -495,4 +508,5 @@ globalThis.__scriptcNodeIsland = {
nextTick,
emit,
load,
sharedPush,
};
+168 -3
View File
@@ -1,4 +1,5 @@
/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs.
/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs
* and scr_node_island_hooks.cjs.
* Do not edit: change the .cjs file and rerun the script. */
#ifndef SCR_NODE_ISLAND_HOST_H
#define SCR_NODE_ISLAND_HOST_H
@@ -228,11 +229,24 @@ 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"
" // A built-in module's name, with or without the node: prefix.\012"
" if (isBuiltin(path)) 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"
@@ -501,6 +515,157 @@ 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, var or function declaration,\012"
"// whose binding JavaScript allows reassigning, 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\" || e.kind === \"function\") {\012"
" // A function declaration's binding is reassignable, like `var`:\012"
" // the island must see the current function, not the first one.\012"
" lines.push(`let ${local} = __s.bind(${mod}, ${slot}, (v) => { ${local} = v; });`);\012"
" lines.push(`export { ${local} as ${name} };`);\012"
" } else {\012"
" lines.push(`const ${local} = __s.get(${mod}, ${slot});`);\012"
" lines.push(`export { ${local} as ${name} };`);\012"
" }\012"
" });\012"
" return lines.join(\"\\n\");\012"
" }\012"
"\012"
" function entrySource(url) {\012"
" const e = entries.get(url);\012"
" const lines = [`import ${JSON.stringify(e.target)};`];\012"
" lines.push('const __s = globalThis[Symbol.for(\"scriptc.shared\")];');\012"
" for (const [mod, slot] of e.late) {\012"
" const n = lateName(mod, slot);\012"
" lines.push(`export let ${n} = __s.late(${mod}, ${slot}, (v) => { ${n} = v; });`);\012"
" }\012"
" return lines.join(\"\\n\");\012"
" }\012"
"\012"
" registerHooks({\012"
" resolve(specifier, context, nextResolve) {\012"
" const asUrl = specifier.startsWith(\"/\") \077 pathToFileURL(specifier).href : specifier;\012"
" if (entries.has(asUrl)) return { url: asUrl, shortCircuit: true };\012"
" const resolved = nextResolve(specifier, context);\012"
" // A shared module keeps its own file URL (require() and import key\012"
" // their module records alike only for file URLs); load answers it.\012"
" if (resolved.url.startsWith(\"file:\") && byUrl.has(resolved.url))\012"
" return { url: resolved.url, format: \"module\", shortCircuit: true };\012"
" return resolved;\012"
" },\012"
" load(url, context, nextLoad) {\012"
" const mod = byUrl.get(url);\012"
" if (mod !== undefined)\012"
" return { format: \"module\", source: sharedSource(mod), shortCircuit: true };\012"
" if (entries.has(url))\012"
" return { format: \"module\", source: entrySource(url), shortCircuit: true };\012"
" return nextLoad(url, context);\012"
" },\012"
" });\012"
"\012"
" return {\012"
" /** The entry to load the island module at `path` through, or null when\012"
" * no shared binding is in its temporal dead zone. Run `load` with it. */\012"
" entry(path, load) {\012"
" const late = JSON.parse(nativeHost.sharedTdz());\012"
" if (late.length === 0) return load(null);\012"
" // A file URL beside the island module (never on disk: the load hook\012"
" // answers it), so require() and import key it alike.\012"
" const url = pathToFileURL(join(dirname(path), `__scriptc_entry_${++entrySeq}.mjs`)).href;\012"
" entries.set(url, { target: pathToFileURL(path).href, late });\012"
" const outer = current;\012"
" current = url;\012"
" try {\012"
" return load(fileURLToPath(url));\012"
" } finally {\012"
" current = outer;\012"
" }\012"
" },\012"
" push(mod, slot, value) {\012"
" for (const set of setters.get(`${mod}:${slot}`) \077\077 []) set(value);\012"
" },\012"
" };\012"
"}\012";
#endif /* SCR_NODE_ISLAND_HOST_H */
@@ -8,6 +8,6 @@ const srcDir = join(import.meta.dirname, "../src");
// scr_node_island.c starts `node -e` with the host embedded in this header;
// a host edit without regenerating it would ship the old host.
test("the embedded Node island host matches its source", () => {
const source = readFileSync(join(srcDir, "scr_node_island_host.cjs"), "utf8");
const source = `${readFileSync(join(srcDir, "scr_node_island_host.cjs"), "utf8")}\n${readFileSync(join(srcDir, "scr_node_island_hooks.cjs"), "utf8")}`;
expect(readFileSync(join(srcDir, "scr_node_island_host.h"), "utf8")).toBe(renderHeader(source));
});
+5 -2
View File
@@ -10,6 +10,8 @@ import { fileURLToPath } from "node:url";
const srcDir = fileURLToPath(new URL("../packages/runtime/src/", import.meta.url));
const SOURCE = `${srcDir}scr_node_island_host.cjs`;
// The loader hooks of shared modules (NI-6) run in the bootstrap's scope.
const HOOKS = `${srcDir}scr_node_island_hooks.cjs`;
const HEADER = `${srcDir}scr_node_island_host.h`;
/** One C string literal per source line, every byte outside printable
@@ -28,7 +30,8 @@ export function renderHeader(source) {
const lines = source.split("\n");
if (lines.at(-1) === "") lines.pop();
const body = lines.map((line) => ` ${cLiteral(`${line}\n`)}`).join("\n");
return `/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs.
return `/* Generated by scripts/node-island-host.mjs from scr_node_island_host.cjs
* and scr_node_island_hooks.cjs.
* Do not edit: change the .cjs file and rerun the script. */
#ifndef SCR_NODE_ISLAND_HOST_H
#define SCR_NODE_ISLAND_HOST_H
@@ -41,7 +44,7 @@ ${body};
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const expected = renderHeader(readFileSync(SOURCE, "utf8"));
const expected = renderHeader(`${readFileSync(SOURCE, "utf8")}\n${readFileSync(HOOKS, "utf8")}`);
if (process.argv.includes("--check")) {
if (readFileSync(HEADER, "utf8") !== expected) {
console.error(`${HEADER} is stale: run node scripts/node-island-host.mjs`);
@@ -1,4 +1,4 @@
exports.timers = () => {
setTimeout(() => console.log("island timer 1"), 15);
setTimeout(() => console.log("island timer 2"), 40);
setTimeout(() => console.log("island timer 1"), 150);
setTimeout(() => console.log("island timer 2"), 250);
};
@@ -7,12 +7,14 @@ import { callSync } from "@scriptc/node";
process.on("uncaughtException", (error) => {
console.log("caught", error.message);
});
// The island's timers (150 and 250 ms) are scheduled before these (10 and
// 50 ms), with gaps wide enough that a slow run keeps the delay order.
callSync("./island.cjs", "timers");
setTimeout(() => {
console.log("native timer throws");
throw new Error("native boom");
}, 5);
setTimeout(() => console.log("native timer after"), 10);
}, 10);
setTimeout(() => console.log("native timer after"), 50);
setImmediate(() => {
queueMicrotask(() => {
throw new Error("from a microtask");
@@ -0,0 +1,46 @@
// @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, describe, retitle } from "./state.mjs";
import { bumpTally, tally } from "./tally.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());
// A module loaded straight into Node is the program's own instance, and
// its members called as methods are the program's functions.
const direct = requireModule("./state.mjs");
const { record: directRecord, entries: directEntries } = direct;
directRecord("direct-1");
console.log("direct load", count, entries(), directEntries() === entries());
console.log("direct methods", direct.record("direct-2"), direct.record(String(count + 1)), direct.entries());
bumpTally();
const { bumpTally: islandBump } = requireModule("./tally.mjs");
console.log("tally", islandBump(), tally);
// A reassigned exported function declaration is a live binding too: the
// island sees the function the program installed.
console.log("describe", plugin.describeNow(), describe());
retitle();
console.log("retitled", plugin.describeNow(), describe());
@@ -0,0 +1,41 @@
// 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, describe } 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();
}
export function describeNow() {
return describe();
}
@@ -0,0 +1,31 @@
// @ts-nocheck -- reassigning the exported function declaration below is the
// behavior under test; a checked file rejects the write.
// 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;
}
// A function declaration whose binding this module reassigns: its export
// is live, like a `let` (a function declaration binds like `var`).
export function describe() {
return "first";
}
export function retitle() {
describe = () => "second";
}
@@ -0,0 +1,8 @@
// A program module no island code imports, loaded into Node only through
// requireModule(): it is still the program's one instance.
export let tally = 0;
export function bumpTally() {
tally++;
return tally;
}
@@ -0,0 +1,35 @@
import { Point, isPoint, make, remember, remembered } from "./shapes.mjs";
export function check(p) {
return `${p instanceof Point} ${isPoint(p)} ${p.describe()} norm=${p.norm()}`;
}
export function create(x, y) {
return new Point(x, y);
}
export function selfCheck() {
const p = new Point(1, 1);
return `${p instanceof Point} ${isPoint(p)} ${make(2, 2) instanceof Point}`;
}
export function plainIsPoint() {
return `${{ x: 1 } instanceof Point} ${isPoint({ x: 1 })}`;
}
export function keep(p) {
return remember(p);
}
export function firstIsNative(p) {
return remembered(0) === p;
}
export function viaFactory() {
const p = make(5, 12);
return `${p.describe()} ${p.norm()} ${p instanceof Point}`;
}
export function sameClass(c) {
return c === Point;
}
@@ -0,0 +1,23 @@
// @requires-libnode
// A class exported by a module both sides import is one class (scope-B
// NI-6): instances made on either side answer instanceof on both, keep
// their identity, and their methods run natively.
import { requireModule } from "@scriptc/node";
import { Point, describeAll, isPoint, make, remember, remembered } from "./shapes.mjs";
const island = requireModule("./island.mjs");
const native = new Point(3, 4);
console.log("island sees native instance:", island.check(native));
const fromIsland = island.create(6, 8);
console.log("native instanceof:", fromIsland instanceof Point, isPoint(fromIsland));
console.log("native reads:", fromIsland.x, fromIsland.y, fromIsland.norm());
console.log("island instanceof:", island.selfCheck());
console.log("plain object (island):", island.plainIsPoint());
console.log("plain object (native):", isPoint({ x: 1, y: 2 }));
remember(native);
console.log("kept:", island.keep(fromIsland), describeAll());
console.log("identity:", remembered(1) === fromIsland, island.firstIsNative(native));
console.log("made by native factory in island:", island.viaFactory());
console.log("class identity:", island.sameClass(Point), island.sameClass(make));
@@ -0,0 +1,35 @@
export class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
norm() {
return Math.hypot(this.x, this.y);
}
describe() {
return `(${this.x},${this.y})`;
}
}
const kept = [];
export function remember(p) {
kept.push(p);
return kept.length;
}
export function remembered(i) {
return kept[i];
}
export function describeAll() {
return kept.map((p) => p.describe()).join(" ");
}
export function make(x, y) {
return new Point(x, y);
}
export function isPoint(p) {
return p instanceof Point;
}
@@ -0,0 +1,10 @@
// @requires-libnode
// An import cycle through a Node island (scope-B NI-6): the compiled package
// `core` imports the island package `ext`, which imports `core` back. As in
// Node, `ext` evaluates first and sees core's functions (hoisted) but its
// let and const bindings in their temporal dead zone; once core's body ran,
// the same bindings read core's live values.
import { check, x } from "core";
console.log("main:", check());
console.log("main x:", x);
@@ -0,0 +1,5 @@
export let x: number;
export const y: string;
export function f(): string;
export function bump(): number;
export function check(): string;
+16
View File
@@ -0,0 +1,16 @@
import { early, later, bumpFromIsland } from "ext";
console.log("core body starts; ext saw:", early);
export let x = 1;
export const y = "y";
export function f() {
return "f";
}
export function bump() {
x++;
return x;
}
export function check() {
return `${later()} ${bumpFromIsland()} ${later()}`;
}
@@ -0,0 +1 @@
{ "name": "core", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,3 @@
export const early: string;
export function later(): string;
export function bumpFromIsland(): string;
+19
View File
@@ -0,0 +1,19 @@
import { bump, f, x, y } from "core";
console.log("ext evaluates; f() =", f());
function probe(read) {
try {
return String(read());
} catch (e) {
return `${e.name}: ${e.message}`;
}
}
export const early = `${probe(() => x)} | ${probe(() => y)}`;
export function later() {
return `x=${x} y=${y}`;
}
export function bumpFromIsland() {
return `bump=${bump()} x=${x}`;
}
@@ -0,0 +1 @@
{ "name": "ext", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "shared-cycle", "private": true, "type": "module", "scriptc": { "packages": { "ext": "node" } } }
@@ -0,0 +1,21 @@
// @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";
import { show } from "banner";
const { run } = requireModule("./scenario.mjs");
const log = (s) => fs.writeSync(1, `${s}\n`);
reg.register(new reg.Plugin("owner-a"));
run(log);
log(`owner banner: ${show()}`);
log(`owner list: ${reg.list().join(",")}`);
log(`owner counter: ${reg.counter}`);
log(`owner describeAll: ${reg.describeAll()}`);
@@ -0,0 +1,2 @@
export declare const label: string;
export declare function show(): string;
@@ -0,0 +1,6 @@
// Looks pure (a const and a function), but its initializer prints: one
// instance must evaluate it once.
export const label = (console.log("banner loaded"), "B");
export function show() {
return label;
}
@@ -0,0 +1 @@
{"name": "banner", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts"}
@@ -0,0 +1,13 @@
export let counter: number;
export function bump(): number;
export class Plugin {
name: string;
constructor(name: string);
describe(): string;
}
export function register(p: { name: string; describe(): string }): number;
// A handle (NI-4 copies no island arrays into typed native arrays).
export function list(): any;
export function find(name: string): Plugin | undefined;
export function describeAll(): string;
export function noop<T>(x: T): T;
@@ -0,0 +1,13 @@
// A stateful singleton: a plugin registry with a mutable exported binding.
export let counter = 0;
export function bump() { return ++counter; }
const items = new Map();
export class Plugin {
constructor(name) { this.name = name; }
describe() { return `plugin:${this.name}`; }
}
export function register(p) { items.set(p.name, p); bump(); return items.size; }
export function list() { return [...items.keys()]; }
export function find(name) { return items.get(name); }
export function describeAll() { return [...items.values()].map((p) => p.describe()).join(' | '); }
export function noop(x) { return x; }
@@ -0,0 +1 @@
{ "name": "registry", "version": "1.0.0", "type": "module", "exports": { "types": "./index.d.ts", "default": "./index.js" } }
@@ -0,0 +1,28 @@
// 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';
import { show } from 'banner';
const require = createRequire(import.meta.url);
export function run(log) {
log(`island banner: ${show()}`);
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,77 @@
// A module namespace taken as a checked-dynamic value: Node's export keys,
// live reads, function typeof, member calls, `in`, for-in, JSON, the
// [object Module] tag, the read-only write TypeErrors and the "Cannot
// convert object to primitive value" coercion, plus `Promise<namespace>`
// widening into `Promise<unknown>` and `unknown`.
import * as declared from "./mod.ts";
import * as expressionDefault from "./mod-default.ts";
function keys(value: unknown): string {
return Object.keys(value as any).join(",");
}
function describe(value: unknown): void {
const dyn = value as any;
console.log("keys", keys(value));
console.log("kinds", valueKinds(value));
console.log("names", Object.getOwnPropertyNames(dyn).join(","));
console.log("alpha", dyn.alpha, typeof dyn.beta, typeof dyn.default);
console.log("call", dyn.beta());
console.log("tag", Object.prototype.toString.call(dyn));
console.log("in", "alpha" in dyn, "toString" in dyn);
const forIn: string[] = [];
for (const key in dyn) forIn.push(key);
console.log("for-in", forIn.join(","));
console.log("json", JSON.stringify(dyn));
try {
console.log("string", String(dyn));
} catch (error) {
console.log("string threw", (error as Error).message);
}
try {
dyn.alpha = 2;
console.log("write", dyn.alpha);
} catch (error) {
console.log("write threw", (error as Error).message);
}
try {
dyn.gamma = 2;
console.log("add", dyn.gamma);
} catch (error) {
console.log("add threw", (error as Error).message);
}
console.log("live", dyn.alpha);
}
function valueKinds(value: unknown): string {
const dyn = value as any;
const kinds: string[] = [];
for (const key of Object.keys(dyn)) kinds.push(typeof dyn[key]);
return kinds.join(",");
}
function bump(value: unknown): void {
const dyn = value as any;
dyn.bump();
console.log("after bump", dyn.count);
}
async function main(): Promise<void> {
// An expression default is snapshotted once by the export statement;
// the boxed namespace's `default` read answers that value, like Node.
const boxedDefault: unknown = expressionDefault;
const dynDefault = boxedDefault as any;
console.log("expression default", dynDefault.default, typeof dynDefault.default);
console.log("expression keys", keys(boxedDefault), expressionDefault.default);
describe(declared);
bump(declared);
const loaded = await import("./mod.ts");
console.log("same", loaded === declared);
describe(loaded);
const widened: Promise<unknown> | undefined = import("./mod.ts");
if (widened) console.log("widened", keys(await widened));
const boxed: unknown = import("./mod.ts");
console.log("unknown", keys(await boxed));
}
main();
@@ -0,0 +1,4 @@
// An EXPRESSION default (`export default <expr>`): Node evaluates the
// expression once, at the export statement, and the namespace object's
// `default` property answers that snapshotted value.
export default 42;
@@ -0,0 +1,12 @@
export const alpha = 1;
export let count = 0;
export function beta(): string {
return "b" + String(count);
}
export function bump(): void {
count += 1;
}
export const obj = { k: alpha };
export default class Gamma {
readonly tag = "g";
}
@@ -0,0 +1,46 @@
// Untyped methods that return their receiver, called as statements on
// receivers whose class is known: side effects, throws, overrides, and
// results that are used.
class Builder {
constructor() {
this.parts = [];
this.count = 0;
}
add(part) {
if (part === "") throw new TypeError("empty part");
this.parts.push(part);
this.count++;
return this;
}
reset() {
this.parts.length = 0;
return this;
}
}
class LoudBuilder extends Builder {
add(part) {
super.add(part.toUpperCase());
return this;
}
}
const b = new Builder();
for (let i = 0; i < 1000; i++) {
b.add("p" + (i % 3));
if (i % 100 === 99) b.reset();
}
console.log(b.count, b.parts.join(","));
b.add("x").add("y");
const same = b.add("z");
console.log(same === b, b.parts.join(","));
try {
b.add("");
} catch (e) {
console.log(e instanceof TypeError, e.message, b.count);
}
const loud = new LoudBuilder();
loud.add("a");
loud.add("b").add("c");
console.log(loud.parts.join(","), loud.count, loud instanceof Builder);
const builders = [new Builder(), new LoudBuilder()];
for (const each of builders) each.add("q");
console.log(builders.map((each) => each.parts.join("")).join("|"));
+97
View File
@@ -0,0 +1,97 @@
// A builtin global in a shorthand property (`{ Uint8Array }`) holds the
// value the long form (`{ Uint8Array: Uint8Array }`) and a plain read hold.
const short = {
Uint8Array,
Int8Array,
Uint8ClampedArray,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Float32Array,
Float64Array,
NaN,
Infinity,
String,
Number,
Boolean,
BigInt,
ArrayBuffer,
Array,
Map,
JSON,
Math,
encodeURIComponent,
decodeURI,
crypto,
Buffer,
fetch,
console,
setTimeout,
};
const long = {
Uint8Array: Uint8Array,
Int8Array: Int8Array,
Uint8ClampedArray: Uint8ClampedArray,
Uint16Array: Uint16Array,
Int16Array: Int16Array,
Uint32Array: Uint32Array,
Int32Array: Int32Array,
Float32Array: Float32Array,
Float64Array: Float64Array,
NaN: NaN,
Infinity: Infinity,
String: String,
Number: Number,
Boolean: Boolean,
BigInt: BigInt,
ArrayBuffer: ArrayBuffer,
Array: Array,
Map: Map,
JSON: JSON,
Math: Math,
encodeURIComponent: encodeURIComponent,
decodeURI: decodeURI,
crypto: crypto,
Buffer: Buffer,
fetch: fetch,
console: console,
setTimeout: setTimeout,
};
for (const key of Object.keys(long)) {
const same = Number.isNaN(long[key]) ? Number.isNaN(short[key]) : short[key] === long[key];
console.log(key, same, typeof short[key]);
}
console.log(short.Uint8Array === Uint8Array, short.Float64Array === Float64Array);
console.log(Object.keys(short).length, Object.keys(short)[0], Object.keys(short)[9]);
// The typed-array constructors construct and keep their names.
const bytes = new short.Uint8Array([1, 2, 300]);
console.log(bytes, bytes instanceof Uint8Array, short.Uint8Array.name, short.Int32Array.BYTES_PER_ELEMENT);
console.log(new short.Float64Array(2).length, short.Int16Array.from([1, -2]));
console.log(short.NaN !== short.NaN, short.Infinity > 1e308, -short.Infinity);
// Shorthand destructuring from the global object reads the same values.
function fromGlobal() {
const { Uint8Array, Float32Array, NaN, Infinity, Buffer } = globalThis;
return [Uint8Array === long.Uint8Array, Float32Array === long.Float32Array, Number.isNaN(NaN), Infinity, Buffer === long.Buffer];
}
console.log(fromGlobal());
// A local binding with a builtin's name is the local.
function shadowed() {
const Uint8Array = "local";
const NaN = 5;
return { Uint8Array, NaN };
}
console.log(shadowed());
// The global object's NaN and Infinity are numbers, read or destructured.
function numbersFromGlobal() {
const { NaN: notANumber, Infinity: infinite } = globalThis;
return [typeof notANumber, notANumber !== notANumber, infinite, infinite === Infinity];
}
console.log(typeof globalThis.NaN, globalThis.NaN !== globalThis.NaN, globalThis.Infinity, -globalThis.Infinity);
console.log(numbersFromGlobal());
+51
View File
@@ -0,0 +1,51 @@
// A builtin global in a shorthand property (`{ Uint8Array }`) holds the
// value the long form (`{ Uint8Array: Uint8Array }`) holds.
const short: Record<string, unknown> = {
Uint8Array,
Int8Array,
Uint8ClampedArray,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Float32Array,
Float64Array,
NaN,
Infinity,
String,
ArrayBuffer,
encodeURIComponent,
};
const long: Record<string, unknown> = {
Uint8Array: Uint8Array,
Int8Array: Int8Array,
Uint8ClampedArray: Uint8ClampedArray,
Uint16Array: Uint16Array,
Int16Array: Int16Array,
Uint32Array: Uint32Array,
Int32Array: Int32Array,
Float32Array: Float32Array,
Float64Array: Float64Array,
NaN: NaN,
Infinity: Infinity,
String: String,
ArrayBuffer: ArrayBuffer,
encodeURIComponent: encodeURIComponent,
};
for (const key of Object.keys(long)) {
const value = long[key];
const same =
typeof value === "number" && Number.isNaN(value)
? Number.isNaN(short[key])
: short[key] === value;
console.log(key, same, typeof short[key]);
}
const numbers = { NaN, Infinity };
console.log(numbers.NaN !== numbers.NaN, numbers.Infinity > 1e308, -numbers.Infinity);
const ctors = { Uint8Array, Float64Array };
const bytes = new ctors.Uint8Array([1, 2, 300]);
console.log(bytes, ctors.Uint8Array.name, new ctors.Float64Array(3).length);
+6 -3
View File
@@ -271,21 +271,24 @@ describe("imports", () => {
it("named and namespace imports keep the loop's timers", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const log: string[] = [];
// The three timers are real and scheduled one after another, so their
// delays are 100 ms apart: a slow run (a sanitized build on a busy
// machine) still sees them fire in delay order.
await Promise.all([
sleep(5).then(() => {
sleep(300).then(() => {
log.push("named promise");
}),
new Promise<void>((resolve) =>
namedSetTimeout(() => {
log.push("named");
resolve();
}, 2),
}, 100),
),
new Promise<void>((resolve) =>
timersNamespace.setTimeout(() => {
log.push("namespace");
resolve();
}, 3),
}, 200),
),
]);
t.mock.timers.tick(10);
@@ -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 }
@@ -0,0 +1 @@
{ "plain": 4451, "san": 57918 }
@@ -0,0 +1 @@
{ "plain": 3100, "san": 4000 }
@@ -0,0 +1 @@
{ "plain": 1800, "san": 2400 }
@@ -0,0 +1,75 @@
/* Which program modules a program shares with its Node islands, and what
* of them the executable carries (scope-B NI-6). Builds to IR only: no
* libnode is needed. */
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, test } from "vitest";
import { compile, type ir } from "@scriptc/compiler";
// Outside any node_modules directory: a program there would be a package.
const cacheDir = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-island-shared-")));
const MAIN = `import { requireModule } from "@scriptc/node";
import { tally } from "./tally.mjs";
const plugin = requireModule("./plugin.mjs");
console.log(tally(), plugin.name);
`;
const TALLY = `// tally: internal counting rules
let n = 0;
export function tally() { return ++n; }
`;
async function irOf(name: string, plugin: string): Promise<ir.IrModule> {
const dir = join(cacheDir, name);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "main.mjs"), MAIN);
writeFileSync(join(dir, "tally.mjs"), TALLY);
writeFileSync(join(dir, "plugin.mjs"), plugin);
const result = await compile(join(dir, "main.mjs"), {
outputKind: "ir",
outPath: join(dir, "program.ir.json"),
outDir: dir,
backend: "llvm",
});
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
return JSON.parse(readFileSync(result.artifact.path, "utf8")) as ir.IrModule;
}
function sharedPaths(mod: ir.IrModule): string[] {
const out: string[] = [];
JSON.stringify(mod, (_k, v: unknown) => {
const o = v as { kind?: string; fn?: string; args?: { kind?: string; value?: string }[] };
if (o !== null && typeof o === "object" && o.kind === "libCall" && o.fn === "nodeIsland.share")
for (const a of o.args ?? [])
if (a.kind === "strLit" && a.value !== undefined) out.push(a.value);
return v;
});
return out;
}
afterAll(() => rmSync(cacheDir, { recursive: true, force: true }));
describe("program modules shared with Node islands", () => {
test("a specifier in a comment of island code shares nothing", async () => {
const mod = await irOf(
"comment",
`// This plugin never imports "./tally.mjs": import { tally } from "./tally.mjs";\nexport const name = "plugin";\n`,
);
expect(sharedPaths(mod).some((p) => p.endsWith("tally.mjs"))).toBe(false);
});
test("a shared module's source stays out of the shipped tree", async () => {
const mod = await irOf(
"import",
`import { tally } from "./tally.mjs";\nexport const name = "plugin:" + tally();\n`,
);
expect(sharedPaths(mod).some((p) => p.endsWith("tally.mjs"))).toBe(true);
const tree = mod.nodeIsland?.modules;
expect(tree).toBeDefined();
const entry = tree!.files.find((f) => f.path.endsWith("tally.mjs"));
expect(entry?.placeholder).toBe(true);
expect(tree!.files.map((f) => f.path).sort()).toEqual(["plugin.mjs", "tally.mjs"]);
});
});
+1 -1
View File
@@ -308,7 +308,7 @@ describe.skipIf(skip)("Node islands in-process", () => {
});
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`,
);
});
+3
View File
@@ -29,6 +29,9 @@ const cases = [
].flatMap((fixture) => ["native", "wasm32-wasi"].map((target) => ({ fixture, target })));
test.for(cases)(
"published $fixture runs statically on $target",
// Bundling three.js and building it with the sanitizer can pass the
// default 300 s on a busy runner, as in three.test.ts.
{ timeout: 1_200_000 },
async ({ fixture, target }, context) => {
if (target === "wasm32-wasi" && !hasZig) context.skip();
const dir = await mkdtemp("/tmp/scriptc-spatial-");