mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Box user module namespaces as dynamic values
A `file:` module namespace handed to typed code now boxes as Node's
namespace object instead of the "converting typed values to 'unknown'"
fence: `const u: unknown = ns`, `take(ns)`, a `Promise<typeof
import("./m")>` widened to `Promise<unknown>` (or `Promise<unknown> |
undefined`), and `await import("./m")` in an `unknown` slot all see one
non-extensible null-prototype object per module whose own properties are
the module's exports in Node's sorted order. They are enumerable
read-only accessors whose reads call the compiled namespace getter, so
they stay live, and the object carries Symbol.toStringTag "Module",
Node's strict-mode write TypeErrors, the not-extensible add error and
the ToPrimitive throw. `node:` builtins have no compiled export table
and keep their SC1101 fence.
The object is built by a new gated runtime unit,
packages/runtime/src/scr_module_namespace_dyn.c, selected by the
`moduleNamespaceDyn` runtime feature (`module.namespaceDyn` in the IR).
It adds no field to ScrDynHandleOps and no branch to a shared unit: the
core reaches it through the ordinary SCR_DYN_OBJ paths, so key listing,
for-in, `in`, JSON, identity, property reads and assignment all reuse
the existing dispatch. The compiler reuses the getter lifted for
module.namespaceValue (existing programs' IR is unchanged) and adds
moduleNamespacePromiseDynValue, one lifted async mapper per module for
the promise slots.
Zero cost: a program that never boxes a namespace links exactly what it
did. The zero-cost gate reports every program identical, with
byte-identical IR and LLVM; the runtime pack's only difference is the
new unit (0 changed runtime functions), and among the namespace corpus
programs only 8412 links it.
Evidence: tests/corpus/8412-module-namespace-untyped is a differential
program against Node in both the plain and sanitized lanes, and the
diagnostics snapshots and TypeScript 7 baselines stay unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a4a844f446
commit
efc22a2fd3
@@ -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,
|
||||
|
||||
@@ -305,6 +305,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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
stdioExtended?: boolean;
|
||||
nodeIsland?: boolean;
|
||||
moduleNamespace?: boolean;
|
||||
moduleNamespaceDyn?: boolean;
|
||||
zlib: boolean;
|
||||
brotli?: boolean;
|
||||
assert: boolean;
|
||||
|
||||
@@ -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 { bindingSymbolOfRead, onlyForwarded, stripTypeWrappers } from "./typed-arguments.js";
|
||||
@@ -189,6 +193,7 @@ import {
|
||||
captureContextBinding,
|
||||
declareContextLocal,
|
||||
declareContextThis,
|
||||
newFnCtx,
|
||||
type FnCtx,
|
||||
} from "./function-context.js";
|
||||
import type { FrontendServices } from "../services.js";
|
||||
@@ -5120,6 +5125,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);
|
||||
}
|
||||
@@ -11869,6 +11947,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.
|
||||
@@ -12016,6 +12109,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,18 +174,14 @@ 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);
|
||||
@@ -263,6 +260,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 +285,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,
|
||||
},
|
||||
|
||||
@@ -695,6 +695,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,
|
||||
|
||||
@@ -3577,6 +3577,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 },
|
||||
|
||||
@@ -1193,6 +1193,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:
|
||||
@@ -1292,6 +1294,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,
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8412-module-namespace-untyped/mod.ts",
|
||||
"<repo>/tests/corpus/8412-module-namespace-untyped/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -90,6 +90,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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";
|
||||
|
||||
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> {
|
||||
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,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 @@
|
||||
{ "plain": 4451, "san": 57918 }
|
||||
Reference in New Issue
Block a user