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 (#2469)
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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
+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": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user