Box user module namespaces as dynamic values (#2469)

This commit is contained in:
Chris Tate
2026-10-28 12:40:34 -05:00
committed by GitHub
21 changed files with 505 additions and 24 deletions
@@ -22,6 +22,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -306,6 +306,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.builtinModule": "scr_process_builtin_module",
"process.builtinUnsupported": "scr_process_builtin_unsupported",
"module.namespaceValue": "scr_module_namespace_value",
"module.namespaceDyn": "scr_module_namespace_dyn_value",
"fs.callbackValue": "scr_fs_callback_value",
"fs.callbackCall": "scr_fs_callback_call",
"process.hrtimeValue": "scr_process_hrtime_value",
@@ -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 {
@@ -201,6 +205,7 @@ import {
captureContextBinding,
declareContextLocal,
declareContextThis,
newFnCtx,
type FnCtx,
} from "./function-context.js";
import type { FrontendServices } from "../services.js";
@@ -5231,6 +5236,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 +12063,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 +12225,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,
},
+1
View File
@@ -702,6 +702,7 @@ async function compileExecutableNative(
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -3604,6 +3604,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 },
@@ -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,
@@ -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": []
}
@@ -93,6 +93,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_perf_loop.c", "perfLoop"],
// Module namespace objects as checked-dynamic values.
["scr_module_namespace.c", "moduleNamespace"],
// The same namespace as the frozen null-prototype object typed code sees.
["scr_module_namespace_dyn.c", "moduleNamespaceDyn"],
// Blob and File box their reads as dyn promises through scr_async_dyn.c.
["scr_blob.c", "dynAsync"],
["scr_zlib.c", "zlib"],
+7
View File
@@ -34,6 +34,13 @@ ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member);
* one handle per module `id`, whose reads call `getter` with the export
* name when `keys` lists it. Borrows all three; +1 handle. */
ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter);
/* A module namespace as checked-dynamic code sees it
* (scr_module_namespace_dyn.c): one non-extensible null-prototype object per
* module `id`, Node's sorted export names as enumerable read-only accessors
* whose getters call `getter` with the name, `Symbol.toStringTag` "Module"
* (the well-known symbol is `tag_key`), and Node's strict-mode write
* TypeErrors. Borrows all four; +1 object. */
ScrDyn *scr_module_namespace_dyn_value(ScrStr *id, ScrArr *keys, ScrDyn *getter, ScrDyn *tag_key);
ScrDyn *scr_global_native(ScrArr *known);
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void));
ScrDyn *scr_console_native(void);
@@ -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,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 @@
{ "plain": 4451, "san": 57918 }