mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
+8
@@ -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": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6851-node-island-shared-class/shapes.mjs",
|
||||
"<repo>/tests/corpus/6851-node-island-shared-class/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6852-node-island-shared-cycle/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6853-node-island-shared-registry/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6854-node-island-shared-registry-native/registry.mjs",
|
||||
"<repo>/tests/corpus/6854-node-island-shared-registry-native/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -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": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/dynamic-method-receiver-statements.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/shorthand-builtin-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export let x: number;
|
||||
export const y: string;
|
||||
export function f(): string;
|
||||
export function bump(): number;
|
||||
export function check(): string;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { early, later, bumpFromIsland } from "ext";
|
||||
|
||||
console.log("core body starts; ext saw:", early);
|
||||
export let x = 1;
|
||||
export const y = "y";
|
||||
export function f() {
|
||||
return "f";
|
||||
}
|
||||
export function bump() {
|
||||
x++;
|
||||
return x;
|
||||
}
|
||||
export function check() {
|
||||
return `${later()} ${bumpFromIsland()} ${later()}`;
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "core", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const early: string;
|
||||
export function later(): string;
|
||||
export function bumpFromIsland(): string;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { bump, f, x, y } from "core";
|
||||
|
||||
console.log("ext evaluates; f() =", f());
|
||||
function probe(read) {
|
||||
try {
|
||||
return String(read());
|
||||
} catch (e) {
|
||||
return `${e.name}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
export const early = `${probe(() => x)} | ${probe(() => y)}`;
|
||||
|
||||
export function later() {
|
||||
return `x=${x} y=${y}`;
|
||||
}
|
||||
|
||||
export function bumpFromIsland() {
|
||||
return `bump=${bump()} x=${x}`;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "ext", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "shared-cycle", "private": true, "type": "module", "scriptc": { "packages": { "ext": "node" } } }
|
||||
@@ -0,0 +1,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()}`);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare const label: string;
|
||||
export declare function show(): string;
|
||||
+6
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"name": "banner", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts"}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export let counter: number;
|
||||
export function bump(): number;
|
||||
export class Plugin {
|
||||
name: string;
|
||||
constructor(name: string);
|
||||
describe(): string;
|
||||
}
|
||||
export function register(p: { name: string; describe(): string }): number;
|
||||
// A handle (NI-4 copies no island arrays into typed native arrays).
|
||||
export function list(): any;
|
||||
export function find(name: string): Plugin | undefined;
|
||||
export function describeAll(): string;
|
||||
export function noop<T>(x: T): T;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// A stateful singleton: a plugin registry with a mutable exported binding.
|
||||
export let counter = 0;
|
||||
export function bump() { return ++counter; }
|
||||
const items = new Map();
|
||||
export class Plugin {
|
||||
constructor(name) { this.name = name; }
|
||||
describe() { return `plugin:${this.name}`; }
|
||||
}
|
||||
export function register(p) { items.set(p.name, p); bump(); return items.size; }
|
||||
export function list() { return [...items.keys()]; }
|
||||
export function find(name) { return items.get(name); }
|
||||
export function describeAll() { return [...items.values()].map((p) => p.describe()).join(' | '); }
|
||||
export function noop(x) { return x; }
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "registry", "version": "1.0.0", "type": "module", "exports": { "types": "./index.d.ts", "default": "./index.js" } }
|
||||
@@ -0,0 +1,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("|"));
|
||||
@@ -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());
|
||||
@@ -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
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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-");
|
||||
|
||||
Reference in New Issue
Block a user