diff --git a/docs/src/app/limitations/page.mdx b/docs/src/app/limitations/page.mdx index 9b45fae34..d919ee640 100644 --- a/docs/src/app/limitations/page.mdx +++ b/docs/src/app/limitations/page.mdx @@ -14,6 +14,7 @@ These are rejected at compile time with an `SC` code, a code frame, and usually - Generics monomorphize when the target resolves statically. The remaining edges include generic functions declared inside another function, generic class expressions, generic classes whose base depends on their own type parameters, and generic methods that would require dynamic dispatch. - Generic function values must be pinned at use to a concrete signature and come from a never-reassigned binding; unpinned or rebound values remain fenced. Immutable aliases of overloaded program functions retain per-call overload resolution and function identity. Typed rest-parameter functions are first-class values: indirect calls pack their surplus arguments into the function's typed array slot. Lowered `node:path` functions, including optional `basename` and variadic `join`/`resolve`, are first-class static values across the bare, POSIX, and win32 modules; the exact zero-argument `node:os` functions and `querystring.escape`/`unescape` are first-class too. The fixed-signature filesystem functions `existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, `closeSync`, and the `node:fs/promises` `unlink`, `chmod`, and `rename` functions are first-class as well. Filesystem APIs with behavior-bearing trailing arguments—including writers, open, directory creation/removal, metadata readers, and directory readers—remain call-only so function-width adaptation cannot discard modes, flags, encodings, or options. Other immutable aliases of table-backed Node builtins can be called directly, while escaping them remains fenced; `util.promisify` has compile-time projections for `child_process.execFile` and utf8 `fs.readFile`. - Spread arguments compile when their arity is static: non-empty fixed tuples flatten into fixed signatures with left-to-right, evaluate-once semantics, while arrays, Sets, and statically represented class iterables spread into typed rest parameters. Runtime-length spreads into fixed signatures remain on the checked-dynamic or island paths in JavaScript and otherwise receive a compile-time diagnostic. +- Literal `import()` of compiled ESM/TypeScript modules and supported Node builtins compiles without the dynamic engine. Evaluation remains lazy and microtask-delayed, top-level `await` is honored, repeated imports share one namespace identity, exported mutable bindings stay live, and `Object.keys` on compiled-module namespaces uses Node's sorted key order. Computed specifiers, import attributes, CommonJS namespace synthesis, and shipped-JavaScript package imports remain explicitly fenced or require `--dynamic`. **Types and shapes** diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 30503487d..bd129488b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -60628,7 +60628,10 @@ "source": "compiler-feature:esm.import", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -60667,7 +60670,10 @@ "source": "compiler-feature:esm.file:", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -60739,7 +60745,10 @@ "source": "compiler-feature:esm.node:", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -60778,7 +60787,10 @@ "source": "compiler-feature:esm.import", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -60817,7 +60829,10 @@ "source": "compiler-feature:esm.import", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -61120,7 +61135,10 @@ "source": "compiler-feature:esm.await", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 876694581..ff1ce0097 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -75230,6 +75230,9 @@ "tests/corpus/2646-top-level-await.ts", "tests/corpus/2701-import-meta/main.mjs", "tests/corpus/2858-import-meta-resolve/main.mjs", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts", "tests/corpus/950-modules-basic/main.ts" ] }, @@ -75531,7 +75534,10 @@ "evidence": "compiler-feature:esm.import", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -75647,7 +75653,10 @@ "evidence": "compiler-feature:esm.file:", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -75709,7 +75718,10 @@ "evidence": "compiler-feature:esm.node:", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -75798,7 +75810,10 @@ "evidence": "compiler-feature:esm.import", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -75860,7 +75875,10 @@ "evidence": "compiler-feature:esm.import", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { @@ -76354,7 +76372,10 @@ "evidence": "compiler-feature:esm.await", "tests": [ "tests/corpus/950-modules-basic/main.ts", - "tests/corpus/2646-top-level-await.ts" + "tests/corpus/2646-top-level-await.ts", + "tests/corpus/2933-static-dynamic-import/main.ts", + "tests/corpus/2934-static-import-stored.ts", + "tests/corpus/2935-module-namespace-values/main.ts" ] }, "dynamic": { diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 8479390f7..6f7963ea3 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -85,7 +85,7 @@ { "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts"] }, { "chapter": "module", "symbols": ["builtinModules"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2932-create-require-program/main.ts"] }, { "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] }, - { "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts"] }, + { "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts", "tests/corpus/2933-static-dynamic-import/main.ts", "tests/corpus/2934-static-import-stored.ts", "tests/corpus/2935-module-namespace-values/main.ts"] }, { "chapter": "esm", "symbols": ["url", "dirname", "filename", "main"], "status": "partial", "evidence": ["tests/corpus/2701-import-meta/main.mjs"] }, { "chapter": "esm", "symbols": ["import.meta.resolve"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs"] }, { "chapter": "esm", "symbols": ["meta", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" }, diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts index b793107ce..c24f45213 100644 --- a/packages/compiler/src/backend/c/exprs.ts +++ b/packages/compiler/src/backend/c/exprs.ts @@ -769,7 +769,7 @@ type ExprOf = Extract; function emitLiteralExpr( emitter: CEmitter, - e: ExprOf<"numLit" | "boolLit" | "strLit" | "unitLit" | "varRef">, + e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">, ): Temp { switch (e.kind) { case "numLit": @@ -780,6 +780,10 @@ function emitLiteralExpr( const sym = emitter.internLiteral(e.value); return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`)); } + case "moduleNsRef": { + const sym = emitter.internLiteral(`module:${e.moduleId}`); + return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`)); + } case "unitLit": // unitLits are consumed inline by the unionWrap case (a unit arm is // tag-only); one reaching the generic dispatch escaped its wrap. @@ -8638,6 +8642,7 @@ export function emitExpr(emitter: CEmitter, e: IrExpr): Temp { case "numLit": case "boolLit": case "strLit": + case "moduleNsRef": case "unitLit": case "varRef": return emitLiteralExpr(emitter, e); diff --git a/packages/compiler/src/backend/c/types.ts b/packages/compiler/src/backend/c/types.ts index bc92291c0..e6e7b81a7 100644 --- a/packages/compiler/src/backend/c/types.ts +++ b/packages/compiler/src/backend/c/types.ts @@ -31,6 +31,8 @@ export function cType(t: IrType): string { return "bool"; case "string": return "ScrStr *"; + case "moduleNs": + return "ScrStr *"; case "bigint": return "ScrBigInt *"; case "array": diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 37e9d0581..3050601c2 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4058,7 +4058,7 @@ class LlEmitter { return this as unknown as LlvmEmitterContext; } - private emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "unitLit" | "varRef">): LlValue { + private emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue { return emitLiteralExpr(this.expressionContext(), e); } diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index 040945e6d..9ecbaba54 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -89,7 +89,7 @@ export interface LlvmEmitterContext extends ShapeHost { emitJsMarshal(e: IrExpr & { kind: "jsMarshal" }): LlValue; emitJsOp(e: IrExpr & { kind: "jsOp" }): LlValue; emitLibCall(e: LibCallExpr): LlValue; - emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "unitLit" | "varRef">): LlValue; + emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue; emitMapLikeIntrinsic(e: Extract): LlValue; emitMapNew(e: IrExpr & { kind: "mapNew" }): LlValue; emitNetworkHttpLibCall(e: LibCallExpr): LlValue; diff --git a/packages/compiler/src/backend/llvm/expr-dispatch.ts b/packages/compiler/src/backend/llvm/expr-dispatch.ts index 4bf324914..33cc18306 100644 --- a/packages/compiler/src/backend/llvm/expr-dispatch.ts +++ b/packages/compiler/src/backend/llvm/expr-dispatch.ts @@ -43,6 +43,7 @@ export function emitExpr(host: LlvmEmitterContext, e: IrExpr): LlValue { case "numLit": case "boolLit": case "strLit": + case "moduleNsRef": case "unitLit": case "varRef": return host.emitLiteralExpr(e); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 66fc8fc65..a7e55e83c 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -16,7 +16,7 @@ const BYTES_ELEM_NUM: Record<"u8" | "u32" | "f32" | "i32", number> = { }; import { f64Lit } from "./common.js"; -export function emitLiteralExpr(host: LlvmEmitterContext, e: ExprOf<"numLit" | "boolLit" | "strLit" | "unitLit" | "varRef">): LlValue { +export function emitLiteralExpr(host: LlvmEmitterContext, e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue { const B = host.B; switch (e.kind) { case "numLit": @@ -27,6 +27,10 @@ export function emitLiteralExpr(host: LlvmEmitterContext, e: ExprOf<"numLit" | " const sym = host.internLiteral(e.value); return host.own({ name: host.retainValue(sym, e.type), type: e.type }); } + case "moduleNsRef": { + const sym = host.internLiteral(`module:${e.moduleId}`); + return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + } case "unitLit": // unitLits are consumed inline by the unionWrap case (a unit arm is // tag-only); one reaching the generic dispatch escaped its wrap. diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index d2b0b6bd9..4477aa2d5 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10,7 +10,7 @@ import { BIGINT_T, BOOL, CAUGHT, DYN, F64, IrExpr, IrFunction, IrLocal, IrParam, import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; import { genResultRecord, isGenericCallableMemberType, typeKey } from "../type-mapper.js"; -import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr } from "./lowerer.js"; +import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr, staticImportNamespaceType } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; import { NARROW_FIRST, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js"; import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js"; @@ -249,6 +249,10 @@ export interface GenericInstance { * - `...xs: T[]`: the ABI type is the array; call sites pack the surplus. */ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape { + const moduleNs = lowerer.moduleNsParamOverrides.get(param); + if (moduleNs !== undefined) { + return { type: moduleNs, mode: param.questionToken ? "omittable" : "required" }; + } // Island-handle params (a then-handler receiving a dynamic import's // namespace handle — markJsvalHandlerParams): jsval, whatever the // contextual type spelled. @@ -6683,16 +6687,20 @@ function loweredTemplateStrings( * to a promise-of-jsval local or module global answers the binding's * type. Null everywhere else. */ function islandPromiseStorageTypeOf(lowerer: Lowerer, e: ts.Expression): IrType | null { - const direct = importCallHandleType(e); + const direct = lowerer.dynamic + ? importCallHandleType(e) + : staticImportNamespaceType(lowerer, e); if (direct?.kind === "promise") return direct; if (!ts.isIdentifier(e)) return null; const local = lowerer.resolveLocal(e); - if (local?.type.kind === "promise" && local.type.inner.kind === "jsval") return local.type; + if (local?.type.kind === "promise" && + (local.type.inner.kind === "jsval" || local.type.inner.kind === "moduleNs")) return local.type; if (local) return null; let sym = lowerer.checker.getSymbolAtLocation(e); if (sym && sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); const g = sym ? lowerer.globalsBySymbol.get(sym) : undefined; - if (g?.type.kind === "promise" && g.type.inner.kind === "jsval") return g.type; + if (g?.type.kind === "promise" && + (g.type.inner.kind === "jsval" || g.type.inner.kind === "moduleNs")) return g.type; return null; } @@ -6711,6 +6719,21 @@ function loweredTemplateStrings( } } + function markModuleNsHandlerParams( + lowerer: Lowerer, + handler: ts.Expression, + type: IrType & { kind: "moduleNs" }, + ): void { + let e = handler; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isArrowFunction(e) && !ts.isFunctionExpression(e)) return; + for (const p of e.parameters) { + if (ts.isIdentifier(p.name) && !p.dotDotDotToken && !p.initializer) { + lowerer.moduleNsParamOverrides.set(p, type); + } + } + } + export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression,): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; @@ -6723,7 +6746,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression // (importCallHandleType / the island-HANDLE var rules), so the storage // type is the receiver's truth. Direct `import("./m").then(...)` // spells the same promise with no binding at all. - if (!recvT && lowerer.dynamic) recvT = islandPromiseStorageTypeOf(lowerer, access.expression); + if (!recvT) recvT = islandPromiseStorageTypeOf(lowerer, access.expression); if (recvT?.kind !== "promise") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); @@ -6829,6 +6852,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression // contextual type spelled (a module-namespace type has no mapping — // the handle is the value's only story, isIslandExpr's local rule). if (inner.kind === "jsval") markJsvalHandlerParams(lowerer, call.arguments[0]!); + if (inner.kind === "moduleNs") markModuleNsHandlerParams(lowerer, call.arguments[0]!, inner); let cb = lowerer.lowerExpr(call.arguments[0]!); // A TYPED handler on a DYN-settling promise (the tracePromise // result's `.then((value) => ...)` — the checker's generic @@ -8431,6 +8455,54 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression if (member !== "keys" && member !== "values" && member !== "entries") return null; if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) return null; const argNode = call.arguments[0]!; + // A compiled PROGRAM module namespace is a nominal token whose members + // remain live bindings, but its enumerable key set is static and Node + // sorts it in code-unit order. Materialize exactly those value-export + // names for Object.keys; values/entries would need a heterogeneous live + // view and stay explicitly fenced. Builtin namespaces also stay fenced: + // their exact runtime export set is Node-owned, not the ambient subset. + { + const probed = tryLowerExpression(lowerer, argNode); + if (probed?.type.kind === "moduleNs") { + if (member !== "keys") { + lowerer.noLowering( + `Object.${member} over a module namespace`, + call, + "read the named exports directly (module namespace values are live and may have heterogeneous representations)", + ); + } + const source = lowerer.sourceFileOfModuleNamespace(probed.type); + if (source === null) { + lowerer.noLowering( + "Object.keys over a builtin module namespace", + call, + "access the builtin's named exports directly (the exact runtime key census is Node-owned)", + ); + } + const moduleSymbol = source ? lowerer.checker.getSymbolAtLocation(source) : undefined; + const names: string[] = []; + moduleSymbol?.getExports().forEach((symbol, key) => { + const name = String(key); + if (name.startsWith("__") || name === "export=") return; + const target = symbol.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(symbol) + : symbol; + if (target.flags & ts.SymbolFlags.Value) names.push(name); + }); + names.sort(); + const loc = locOf(call); + const result: IrExpr = { + kind: "arrayLit", + elems: names.map((name) => ({ kind: "strLit", value: name, type: STRING, loc })), + type: arrayOf(STRING), + loc, + }; + const receiver = lowerer.lowerExpr(argNode); + return isSafeToDiscard(receiver) + ? result + : { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: receiver, loc }], result, type: result.type, loc }; + } + } // A CHECKED-DYNAMIC argument — the checker may still spell a record // type (the JS file-scope object-literal identity story stores the // dyn object), so the LOWERED value's kind is the dispatch: the diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b4bc2605b..4de0dc1d2 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -664,6 +664,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { symbol: "symbol", map: "object", set: "object", promise: "object", bytes: "object", regexp: "object", generator: "object", classval: "function", + moduleNs: "object", undefinedT: "undefined", nullT: "object", }; const folded = FOLD[operand.type.kind]; @@ -972,7 +973,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // the namespace object has no representation — members lower at // their access sites only. The CommonJS namespace binding // (`const lib = require("./lib.js")`) gets the same fence. - if (lowerer.builtinNamespaceModuleOf(expr) !== null || lowerer.cjsLocalModuleBindingOf(expr)) { + const builtinNamespace = lowerer.builtinNamespaceModuleOf(expr); + if (builtinNamespace !== null) { + const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNamespace}` }; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; + } + if (lowerer.cjsLocalModuleBindingOf(expr)) { lowerer.unsupported( "SC1090", expr, @@ -1182,6 +1188,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // representation (ambient namespaces compile to Node's // ReferenceError instead — the object never exists at runtime). { + const moduleNs = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (moduleNs?.kind === "moduleNs") { + return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc }; + } + const builtinNs = lowerer.builtinNamespaceModuleOf(expr); + if (builtinNs !== null) { + const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNs}` }; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; + } const ns = lowerNsIdentifierValue(lowerer, expr); if (ns) return ns; } @@ -6725,6 +6740,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (idLeft.type.kind === "classval" && idRight.type.kind === "classval") { return { kind: "bin", op: negated ? "!==" : "===", left: idLeft, right: idRight, type: BOOL, loc }; } + if (idLeft.type.kind === "moduleNs" && idRight.type.kind === "moduleNs") { + return { kind: "bin", op: negated ? "!==" : "===", left: idLeft, right: idRight, type: BOOL, loc }; + } if ( (idLeft.type.kind === "array" || idLeft.type.kind === "map" || diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index 9234a5fc6..e9a0173d4 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -2678,7 +2678,9 @@ export function lowerStaticReadableStreamReaderCall( ); } -/** Dynamic `import(spec)` — the island's module system at a USER site. +/** Literal `import(spec)` at a user site. Static builds compile program + * modules and supported Node builtins to a native Promise; + * dynamic builds retain the island-backed package/module path below. * Under --dynamic the call lowers to island.importDyn(key): the engine * loads the module (embedded npm graph, a shipped local .js/.mjs the * build embedded, or a builtin shim — collectDynamicImports resolved and @@ -2689,11 +2691,11 @@ export function lowerStaticReadableStreamReaderCall( * a load/evaluation failure crosses as a catchable rejection, exactly * where Node puts it. Specifiers must be string literals: the module * graph is a BUILD-time artifact — a runtime-computed name has nothing - * to embed, and the fence says so. Static builds report the per-site - * SC2012. Null for anything that isn't `import(...)`. */ + * to embed, and the fence says so. Static builds accept compiled program + * modules and supported builtins; other execution homes report SC2012. + * Null for anything that isn't `import(...)`. */ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; - lowerer.requireDynamicApi("'import()'", call); const loc = locOf(call); const arg = call.arguments[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) { @@ -2708,13 +2710,27 @@ export function lowerStaticReadableStreamReaderCall( lowerer.unsupported("SC1090", call, "dynamic import() with import attributes"); } const res = lowerer.dynImports.get(`${call.getSourceFile().fileName}\u0000${arg.text}`); + if (!lowerer.dynamic) { + if (res?.kind === "program-module") { + return lowerOwnStaticModuleImport(lowerer, call, arg); + } + if (res?.kind === "static-builtin") { + return lowerStaticNamespaceImport(lowerer, call, { + kind: "moduleNs", + moduleId: `builtin:${res.module}`, + }); + } + lowerer.requireDynamicApi("'import()'", call); + throw new PoisonError(); + } + lowerer.requireDynamicApi("'import()'", call); if (!res) { // Collection walks every file before bodies lower, so a missing // entry is a lowerer bug, not user error. throw new InternalCompilerError(`lowerer bug: unresolved dynamic import '${arg.text}'`); } if (res.kind === "program-module") { - return lowerOwnModuleImport(lowerer, call, arg); + return lowerOwnDynamicModuleImport(lowerer, call, arg); } if (res.kind !== "module") { throw new PoisonError(); // resolution failed — collection reported it @@ -2745,7 +2761,7 @@ export function lowerStaticReadableStreamReaderCall( * signatures) cross as trap functions that throw a pointed TypeError * when USED — the namespace still builds, exactly like Node still * resolves it. */ - function lowerOwnModuleImport(lowerer: Lowerer, call: ts.CallExpression, arg: ts.StringLiteralLike): IrExpr { + function lowerOwnDynamicModuleImport(lowerer: Lowerer, call: ts.CallExpression, arg: ts.StringLiteralLike): IrExpr { const loc = locOf(call); let dep: ts.SourceFile | null = null; const modSym = lowerer.checker.getSymbolAtLocation(arg); @@ -2791,6 +2807,170 @@ export function lowerStaticReadableStreamReaderCall( return { kind: "jsBridgePromise", value: chained, type: { kind: "promise", inner: JSVAL }, loc }; } +/** The checker-resolved program source behind one literal import(). */ + function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.SourceFile | null { + const modSym = lowerer.checker.getSymbolAtLocation(arg); + for (const d of (modSym ? lowerer.checker.declarationsOf(modSym) : [])) { + if (ts.isSourceFile(d) && !d.isDeclarationFile) return d; + } + return null; + } + +/** Static literal import of one compiled ESM module. The async helper's + * first operation is an await of an already-settled promise: evaluation + * therefore starts on a microtask, after the importer's synchronous + * tail, matching import(). Its result is the module's singleton nominal + * namespace token; member reads resolve separately to live exports. */ + function lowerOwnStaticModuleImport( + lowerer: Lowerer, + call: ts.CallExpression, + arg: ts.StringLiteralLike, + ): IrExpr { + const dep = programImportTarget(lowerer, arg); + if (dep !== null && (dep.fileName.endsWith(".cts") || isCjsJsFile(dep))) { + lowerer.unsupported( + "SC1090", + call, + `dynamic import of the program's own CommonJS module '${arg.text}' ` + + "(its namespace comes from module.exports through Node's CJS lexer, " + + "which has no compiled story — require it, or import it statically)", + ); + } + if (dep === null || lowerer.initNameOf.get(dep) === undefined) { + lowerer.unsupported( + "SC1090", + call, + `dynamic import of the program's own module '${arg.text}' ` + + "(this module is not part of the compiled module graph)", + ); + } + const ns: IrType & { kind: "moduleNs" } = { + kind: "moduleNs", + moduleId: `file:${dep === null ? "" : lowerer.moduleNamespaceIdOfType(lowerer.typeOf(call))?.replace(/^file:/, "") ?? dep.fileName}`, + }; + // The call's type is Promise; prefer the mapper's + // canonical normalized id when available. + const mapped = lowerer.mapTypeOf(lowerer.typeOf(call)); + const inner = mapped?.kind === "promise" && mapped.inner.kind === "moduleNs" ? mapped.inner : ns; + return lowerStaticNamespaceImport(lowerer, call, inner, dep); + } + +/** Builds a fresh native promise for a static module namespace. Source + * modules run/await their guarded initializer after the mandatory hop; + * builtins only take the hop and resolve their immortal token. */ + function lowerStaticNamespaceImport( + lowerer: Lowerer, + call: ts.CallExpression, + ns: IrType & { kind: "moduleNs" }, + dep: ts.SourceFile | null = null, + ): IrExpr { + const loc = locOf(call); + const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; + const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; + const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; + const fnCtx = newFnCtx(true, null, fnType, ns); + fnCtx.isAsync = true; + lowerer.fnStack.push(fnCtx); + try { + const hop: IrExpr = { + kind: "awaitExpr", + value: { + kind: "intrinsic", + name: "promise.resolve", + args: [], + type: { kind: "promise", inner: VOID }, + loc, + }, + type: VOID, + loc, + }; + const body: IrStmt[] = [{ kind: "exprStmt", expr: hop, loc }]; + if (dep !== null) appendStaticModuleInit(lowerer, dep, body, loc); + body.push({ + kind: "return", + value: { kind: "moduleNsRef", moduleId: ns.moduleId, type: ns, loc }, + loc, + }); + const ctx = lowerer.ctx; + lowerer.liftedFns.push({ + name: fnName, + params: [], + returnType: ns, + locals: ctx.locals, + captures: ctx.captures ?? [], + body, + async: true, + loc, + }); + } finally { + lowerer.fnStack.pop(); + } + return { + kind: "callValue", + callee: { kind: "closure", fnName, captures: [], type: fnType, loc }, + args: [], + type: resultT, + loc, + }; + } + +/** Appends Node's lazy source-module link/evaluation work to a static + * import helper. This is the native twin of dynNsBuilderOf's prelude. */ + function appendStaticModuleInit( + lowerer: Lowerer, + dep: ts.SourceFile, + body: IrStmt[], + loc: IrExpr["loc"], + ): void { + const linkCrash = esmNamedImportLinkCrash(lowerer.program, dep); + if (linkCrash !== null) { + body.push({ + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "strLit", value: linkCrash.message, type: STRING, loc }], + type: { kind: "object", className: linkCrash.className }, + loc, + }, + loc, + }); + return; + } + const initName = lowerer.initNameOf.get(dep); + if (initName === undefined) throw new InternalCompilerError("lowerer bug: static import target has no init"); + const isAsync = lowerer.asyncInitFiles.has(dep); + if (dep === lowerer.entry && !isAsync) return; + const initCall: IrExpr = { + kind: "call", + callee: initName, + args: [], + type: isAsync ? { kind: "promise", inner: VOID } : VOID, + loc, + }; + const cyclePromiseId = lowerer.asyncCyclePromiseOf.get(dep); + if (isAsync && cyclePromiseId !== undefined) { + body.push({ kind: "exprStmt", expr: initCall, loc }); + const promiseT: IrType = { kind: "promise", inner: VOID }; + body.push({ + kind: "exprStmt", + expr: { + kind: "awaitExpr", + value: { kind: "varRef", localId: cyclePromiseId, type: promiseT, loc }, + type: VOID, + loc, + }, + loc, + }); + return; + } + body.push({ + kind: "exprStmt", + expr: isAsync ? { kind: "awaitExpr", value: initCall, type: VOID, loc } : initCall, + loc, + }); + } + /** The namespace-BUILDER function for one program module, synthesized on * first demand and shared by every import() of that module (deterministic * name per file tag, so the discovery and emit passes agree). Body: the diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index ea9d503b7..db2247454 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -13,7 +13,7 @@ import { canonicalBuiltinModule, cjsExportAssignmentOf, cjsExportDiscardReason, import type { CycleEdge } from "../program.js"; import { invalidJsonModuleDiag, npmEmbedFailedDiag, requiresDynamicImportDiag } from "../../diagnostics/diagnostic.js"; import { BOOL, DYN, IrClassDef, IrExpr, IrFunction, IrGlobal, IrRecordShape, IrStmt, IrType, IrUnionDef, JSVAL, RUNTIME_ERROR_CLASSES, STRING, SrcLoc, VOID, arrayOf, canConvertToDyn, isUnitType } from "../../ir/ir.js"; -import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, uncheckedOverloadHandleCall } from "./lowerer.js"; +import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, staticImportNamespaceType, uncheckedOverloadHandleCall } from "./lowerer.js"; import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, textCodecBindingDecl } from "./lower-builtins.js"; import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js"; import { isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js"; @@ -384,8 +384,8 @@ export interface FileParts { } } } + for (const fp of parts) collectDynamicImports(lowerer, builder, fp.sf); if (builder) { - for (const fp of parts) collectDynamicImports(lowerer, builder, fp.sf); for (const fp of parts) collectCreateRequires(lowerer, builder, fp.sf); const graph = builder.finish(); if (graph.modules.length > 0) { @@ -396,19 +396,21 @@ export interface FileParts { } } -/** The dynamic-import half of the npm chokepoint (--dynamic only): every +/** Literal dynamic-import collection: every * `import("literal")` in the file — whatever body it sits in — resolves * and embeds AT COLLECTION time, so the per-site lowering later just * looks its key up (the emitted tables are assembled once, here; a * site lowered in any pass finds its module embedded). Resolution + * In static builds this records compiled program modules and supported + * builtins without constructing an npm graph. Dynamic package-resolution * failures and unshimmed builtins are diagnostics HERE, at the import * expression, exactly like static npm imports at their statements; the * lowering poisons those sites without re-reporting. Non-literal * specifiers are skipped — the lowering owns that fence (the module * graph is a build-time artifact; there is nothing to embed for a * runtime-computed name). */ - function collectDynamicImports(lowerer: Lowerer, builder: NpmGraphBuilder, sf: ts.SourceFile): void { - const visit = (node: ts.Node): void => { + function collectDynamicImports(lowerer: Lowerer, builder: NpmGraphBuilder | null, sf: ts.SourceFile): void { + ts.walkPreorder(sf, (node) => { if ( ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && @@ -418,8 +420,7 @@ export interface FileParts { ) { const spec = node.arguments[0].text; if (lowerer.externalTypes.has(spec)) { - ts.forEachChild(node, visit); - return; + return undefined; } const mapKey = `${sf.fileName}\u0000${spec}`; if (!lowerer.dynImports.has(mapKey)) { @@ -432,9 +433,15 @@ export interface FileParts { const ownModule = modSym !== undefined && lowerer.checker.declarationsOf(modSym).some( (d) => ts.isSourceFile(d) && !d.isDeclarationFile, ); + const builtin = canonicalBuiltinModule(spec); const res = ownModule ? ({ kind: "program-module" } as const) - : builder.addDynamicImport(sf.fileName, spec); + : !lowerer.dynamic && builtin !== null + ? ({ kind: "static-builtin", module: builtin } as const) + : builder?.addDynamicImport(sf.fileName, spec); + if (res === undefined) { + return undefined; + } lowerer.dynImports.set(mapKey, res); if (res.kind === "unsupported-builtin") { lowerer.pushDiag( @@ -448,9 +455,8 @@ export interface FileParts { } } } - ts.forEachChild(node, visit); - }; - visit(sf); + return undefined; + }); } /** The createRequire half of the npm chokepoint (--dynamic only): every @@ -1441,11 +1447,12 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts // (lowerVarDecl's rule at module scope; the init body assigns // it). const handleT = - lowerer.dynamic && ts.isIdentifier(decl.name) && nameNode === decl.name - ? (importCallHandleType(decl.initializer) ?? + ts.isIdentifier(decl.name) && nameNode === decl.name + ? (lowerer.dynamic ? importCallHandleType(decl.initializer) : staticImportNamespaceType(lowerer, decl.initializer)) ?? + (lowerer.dynamic ? // An unchecked-overload call result stores the handle, // exactly the local rule (uncheckedOverloadHandleCall). - (uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null)) + (uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null) : null) : null; let type = handleT ?? lowerer.irTypeOf(nameNode); // An evolving-`any` array's DERIVED file-scope binding under diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 31c4f570a..b4190c617 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -220,6 +220,12 @@ function nsBlockKindOfSymbol(lowerer: Lowerer, sym: ts.Symbol): "flattened" | "t * .d.ts files) and npm packages answer null — their own chokepoints and * fences keep ownership. */ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile | null { + const stored = ts.isIdentifier(e) + ? (lowerer.peekLocal(e)?.type ?? lowerer.globalOf(e)?.type) + : undefined; + const mapped = stored ?? lowerer.mapTypeOf(lowerer.typeOf(e)); + const typedSource = mapped ? lowerer.sourceFileOfModuleNamespace(mapped) : null; + if (typedSource !== null) return typedSource; let sym: ts.Symbol | undefined; if (ts.isIdentifier(e)) { sym = lowerer.checker.getSymbolAtLocation(e); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 6a31113e8..e6354f825 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -9,7 +9,7 @@ import { arrayValueRead, arrayValueStore, arrayValueType } from "./array-values. import { lowerForAwaitGenerator, lowerForOfGenerator, lowerYieldStarStatement, type GenType } from "./lower-generators.js"; import { lowerForAwaitBuiltin } from "./lower-async-iteration.js"; import { BOOL, BYTES_U8, CAUGHT, DYN, F64, IrExpr, IrGlobal, IrJsOp, IrLocal, IrStmt, IrType, JSVAL, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; -import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js"; +import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, staticImportNamespaceType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isJsSourceFile, locOf, requireSpecOf } from "../program.js"; import { COMPOUND_ASSIGN_OPS, CompoundOp, STR_METHODS, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js"; @@ -686,6 +686,9 @@ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDec (uncheckedOverloadHandleCall(lowerer, nameNode.parent.initializer) ? JSVAL : null) ?? type; } + if (!lowerer.dynamic && ts.isVariableDeclaration(nameNode.parent) && nameNode.parent.name === nameNode) { + type = staticImportNamespaceType(lowerer, nameNode.parent.initializer) ?? type; + } if (!type || type.kind === "void") return null; return type; } @@ -3765,6 +3768,8 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL // island members). (lowerer.dynamic && init.type.kind === "dyn" && jsvalFlavoredType(lowerer.mapTypeOf(lowerer.typeOf(decl.name)) ?? DYN) ? DYN : null) ?? (bindingTainted ? null : lowerer.mapTypeOf(lowerer.typeOf(decl.name))) ?? + staticImportNamespaceType(lowerer, decl.initializer) ?? + (init.type.kind === "moduleNs" ? init.type : null) ?? (init.type.kind === "dyn" ? DYN : null); if ( lowerer.implicitParamTypes !== null && diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6f402c0d3..e038919d6 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -63,6 +63,7 @@ import { cjsExportDiscardReason, fallbackDtsPath, isCjsExportTableLiteral, + isCjsJsFile, isJsSourceFile, isNodeEsmFile, isNodeTypesPath, @@ -514,21 +515,19 @@ export function lowerToIr( const dynamic = options.dynamic ?? false; const targetPlatform = options.targetPlatform ?? process.platform; const startupCrash = options.startupCrash ?? null; - // --dynamic: modules reachable only through dynamic import() of the - // program's own files join the compiled graph here, ONCE, before any + // Modules reachable only through literal import() of the program's own + // files join the compiled graph here, ONCE, before any // pass constructs (nothing calls their %init at startup — the import() // site's namespace builder does, on the engine microtask, Node's // evaluation point for them). Inadmissible static cycles inside the // added subgraph are minted here and handed to reachable emit after this // extension of the shared array; no later pass re-walks the subgraph. const dynamicCycleDiags: ScrDiagnostic[] = []; - if (dynamic) { - appendDynamicImportModules(program, moduleOrder, (cycle, reason) => { - dynamicCycleDiags.push( - unsupportedDiag("SC1016", { file: entry.fileName, start: 0, end: 0 }, `circular imports (${cycle}; ${reason})`), - ); - }); - } + appendDynamicImportModules(program, moduleOrder, (cycle, reason) => { + dynamicCycleDiags.push( + unsupportedDiag("SC1016", { file: entry.fileName, start: 0, end: 0 }, `circular imports (${cycle}; ${reason})`), + ); + }); const ffiImports = options.ffiImports ?? []; const libraryCallbacks = options.libraryCallbacks ?? false; const externalTypes = options.externalTypes ?? new Map(); @@ -632,6 +631,44 @@ export function importCallHandleType(expr: ts.Expression | undefined): IrType | return null; } +/** The native namespace type produced by a literal static import() of a + * compiled program module or supported Node builtin. This syntax-level + * fallback is used where the checker exposes an anonymous builtin-module + * object type with no stable module symbol (locals/globals are still + * represented nominally by the value they actually receive). */ +export function staticImportNamespaceType(lowerer: Lowerer, expr: ts.Expression | undefined): IrType | null { + if (!expr || lowerer.dynamic) return null; + let e = expr; + let awaited = false; + for (;;) { + if (ts.isParenthesizedExpression(e)) e = e.expression; + else if (ts.isAwaitExpression(e)) { + awaited = true; + e = e.expression; + } else break; + } + if (!ts.isCallExpression(e) || e.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; + const arg = e.arguments[0]; + if (!arg || !ts.isStringLiteralLike(arg)) return null; + const builtin = canonicalBuiltinModule(arg.text); + let moduleId: string | null = builtin === null ? null : `builtin:${builtin}`; + if (moduleId === null) { + const symbol = lowerer.checker.getSymbolAtLocation(arg); + const source = symbol && lowerer.checker.declarationsOf(symbol).find( + (decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile, + ); + if ( + source && !source.fileName.endsWith(".cts") && !isCjsJsFile(source) && + lowerer.moduleOrder.includes(source) + ) { + moduleId = `file:${tsgoPath(resolve(source.fileName))}`; + } + } + if (moduleId === null) return null; + const ns: IrType = { kind: "moduleNs", moduleId }; + return awaited ? ns : { kind: "promise", inner: ns }; +} + /** True when `expr` is a call that resolved to an overload SIGNATURE of a * source-implemented function whose implementation returns an island value * (`any` under --dynamic): tsc never checks overload return types against @@ -1653,6 +1690,10 @@ export class Lowerer { * engine handle (a dynamic import's namespace object) — paramShape's * early-out. */ readonly jsvalParamOverrides = new Set(); + /** Inline Promise.then parameters whose contextual type is a builtin + * module namespace anonymous object. The settled native token is the + * ABI truth even when the checker type has no stable module symbol. */ + readonly moduleNsParamOverrides = new Map(); /** File → qualifier prefix: "" for the entry, "%mI." otherwise. */ readonly fileTag = new Map(); /** Namespace ModuleBlocks this program lowers, filled by splitFiles: @@ -1904,8 +1945,9 @@ export class Lowerer { // fileTag is filled just below; the hook is only ever CALLED during // lowering, long after the constructor completes. isProgramFile: (sf) => this.fileTag.has(sf), + moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type), }; - // --dynamic: modules reachable only through dynamic import() joined + // Modules reachable only through literal import() joined // moduleOrder BEFORE any pass constructed — lowerToIr runs // appendDynamicImportModules once on the shared array (a per-pass run // here would repeatedly extend the graph and duplicate cycle reports). @@ -1922,6 +1964,35 @@ export class Lowerer { return registerBuiltinErrorClasses(this); } + /** The nominal identity of a checker module-namespace type. Program + * modules use their normalized absolute source name; supported builtins + * use the lowering tables' canonical bare name. */ + moduleNamespaceIdOfType(type: ts.Type): string | null { + let sym = type.getSymbol(); + if (!sym) return null; + if (sym.flags & ts.SymbolFlags.Alias) sym = this.checker.getAliasedSymbol(sym); + for (const decl of this.checker.declarationsOf(sym)) { + if ( + ts.isSourceFile(decl) && !decl.isDeclarationFile && + !decl.fileName.endsWith(".cts") && !isCjsJsFile(decl) && + this.moduleOrder.includes(decl) + ) { + return `file:${tsgoPath(resolve(decl.fileName))}`; + } + if (ts.isModuleDeclaration(decl) && ts.isStringLiteral(decl.name)) { + const builtin = canonicalBuiltinModule(decl.name.text); + if (builtin !== null) return `builtin:${builtin}`; + } + } + return null; + } + + sourceFileOfModuleNamespace(type: IrType): ts.SourceFile | null { + if (type.kind !== "moduleNs" || !type.moduleId.startsWith("file:")) return null; + const path = type.moduleId.slice("file:".length); + return this.moduleOrder.find((sf) => tsgoPath(resolve(sf.fileName)) === path) ?? null; + } + builtinErrorInfoOf(symbol: ts.Symbol | null | undefined): ClassInfo | null { return builtinErrorInfoOf(this, symbol); } @@ -3607,7 +3678,7 @@ export class Lowerer { this.diags.length > 0 ? null : { - irVersion: 10, + irVersion: 11, sourceFile: this.entry.fileName, functions, classes: artifacts.classes, @@ -9818,6 +9889,13 @@ export class Lowerer { * member access that IS a module in its own right (`fs.promises` — the * same object as node:fs/promises, Node's rule). Null otherwise. */ builtinNamespaceModuleOf(expr: ts.Expression): string | null { + const stored = ts.isIdentifier(expr) + ? (this.peekLocal(expr)?.type ?? this.globalOf(expr)?.type) + : undefined; + const mapped = stored ?? this.mapTypeOf(this.typeOf(expr)); + if (mapped?.kind === "moduleNs" && mapped.moduleId.startsWith("builtin:")) { + return mapped.moduleId.slice("builtin:".length); + } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; diff --git a/packages/compiler/src/frontend/npm.ts b/packages/compiler/src/frontend/npm.ts index d9ec6b794..9aad9784a 100644 --- a/packages/compiler/src/frontend/npm.ts +++ b/packages/compiler/src/frontend/npm.ts @@ -184,9 +184,13 @@ export type DynamicImportResolution = /** A Node builtin the island ships no shim for — a build diagnostic, * exactly like a static import of it. */ | { kind: "unsupported-builtin"; builtin: string } - /** The specifier resolves into the compiled program's own TypeScript — - * fenced at the site (static imports are the way in). */ + /** The specifier resolves into the compiled program's own TypeScript. + * Static builds expose its nominal live namespace; dynamic builds + * marshal a snapshot namespace into the island. */ | { kind: "program-module" } + /** A supported Node builtin imported by a static build. Its namespace + * token routes member reads through the existing builtin tables. */ + | { kind: "static-builtin"; module: string } /** No resolution — a build diagnostic, like static imports. */ | { kind: "unresolved"; message: string }; @@ -1020,7 +1024,7 @@ export class NpmGraphBuilder { * see, because tsc compiles those — resolve against the importing FILE * with Node's file/extension/directory rules and embed the reached JS * graph. A relative target that is TypeScript is the program's own - * module (fenced at the site: static imports are the way in), and one + * module (compiled natively and exposed through the import lowering), and one * with an extension no loader executes (.wasm, .node, ...) embeds a stub * whose EVALUATION throws Node's exact ERR_UNKNOWN_FILE_EXTENSION * TypeError — the build succeeds and the import() rejects at runtime, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 5ce4cdd40..fd20107f4 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -540,6 +540,8 @@ export function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegi return "Writable"; case "procStream": return "WriteStream"; + case "moduleNs": + return `module namespace '${t.moduleId}'`; case "promise": return `Promise<${formatIrType(t.inner, shapes, unions, seen)}>`; case "generator": @@ -734,6 +736,11 @@ export interface TypeMapperCtx { * would ICE the validator. Such instance types stay unmapped (null): * callers fence them like any other unsupported type. */ isProgramFile: (sf: ts.SourceFile) => boolean; + /** Static ECMAScript module namespace identity, when `type` is exactly a + * compiled source module or supported Node builtin namespace. Dynamic + * builds leave these unmapped because import() values live in the + * embedded engine there. */ + moduleNamespaceId?: (type: ts.Type) => string | null; } @@ -873,6 +880,10 @@ function classExprNeverRegisters(decl: ts.ClassLikeDeclaration): boolean { function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const { checker, unions, classNamer, resolveTypeParam } = ctx; + if (!ctx.dynamic) { + const moduleId = ctx.moduleNamespaceId?.(type) ?? null; + if (moduleId !== null) return { kind: "moduleNs", moduleId }; + } // @types/node spells child_process.ExecFileException as an intersection // type alias over two Error-derived metadata interfaces. The value is // still one ordinary Error; recognize the owned alias before generic @@ -1095,15 +1106,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return ctx.dynamic ? JSVAL : null; } - // NOTE on module NAMESPACE types (`typeof import("./x.mjs")` — what a - // dynamic import resolves to): non-stdlib ones fall under the rule - // above (their declarations are the .d.ts SourceFiles themselves). - // Stdlib `declare module` namespaces ("fs", "path") deliberately stay - // unmapped: their members have STATIC lowerings keyed off import - // bindings (builtinImportOf), and a handle mapping would reroute - // `import * as path` member calls into the engine — dynamic imports of - // builtins get their handles from the import lowering's IR type - // instead. + // Module NAMESPACE types (`typeof import("./x.mjs")`) were claimed at + // the top by moduleNamespaceId when they name a compiled source module + // or supported builtin in a static build. Declaration-only package + // namespaces retain the npm rule above; dynamic builds retain their + // jsval import-handle path. // T[]: monomorphic arrays, recursively (number[][] works). An element type // that doesn't map (never from a context-free `[]`) makes the whole array // unsupported — null propagates. Record/object/union elements ride the diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 61f42ac1b..b3a99d76f 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -254,6 +254,12 @@ export type IrType = * elements, Map VALUES, and union arms; fenced out of Map keys, Set * elements, JSON, dyn/jsval conversion, and ToString. */ | { kind: "classval"; className: string } + /** An ECMAScript module namespace object for one statically-known module. + * `moduleId` is a canonical compiled source-file or builtin identity. + * Runtime representation is an interned immortal string token: identity + * is pointer identity, while member reads resolve to live module globals + * or builtin lowering tables rather than snapshotting export values. */ + | { kind: "moduleNs"; moduleId: string } /** Structural record shape (object literal / interface / type alias over * data properties). `shapeId` indexes IrModule.records; the frontend * interns shapes structurally, so equal shapeId ⇔ equal shape and @@ -386,6 +392,7 @@ const POINTER_KIND_LIST = [ "func", "object", "classval", + "moduleNs", "record", "union", "dyn", @@ -438,6 +445,7 @@ export const RUNTIME_RC_STEMS: Record = { func: "scr_closure", object: "", classval: "scr_classobj", + moduleNs: "scr_str", record: "", union: "scr_union", dyn: "scr_dyn", @@ -478,6 +486,8 @@ export const REF_TRUTHY_KINDS: ReadonlySet = new Set([ "generator", // A class object is a JS object (constructors are functions): always truthy. "classval", + // A module namespace object is always truthy. + "moduleNs", ]); export const F64: IrType = { kind: "f64" }; @@ -549,6 +559,7 @@ export function isSupportedArrayElem(t: IrType): boolean { case "netServer": case "symbol": case "classval": + case "moduleNs": return true; default: return false; @@ -732,6 +743,8 @@ export function typeKey(t: IrType): string { return `object:${t.className}`; case "classval": return `classval:${t.className}`; + case "moduleNs": + return `moduleNs:${t.moduleId}`; case "record": return `record:${t.shapeId}`; case "union": @@ -767,6 +780,7 @@ export function typeEquals(a: IrType, b: IrType): boolean { } if (a.kind === "object") return b.kind === "object" && a.className === b.className; if (a.kind === "classval") return b.kind === "classval" && a.className === b.className; + if (a.kind === "moduleNs") return b.kind === "moduleNs" && a.moduleId === b.moduleId; // The frontend deduplicates shapes structurally (one shapeId per canonical // field list), so id equality IS structural equality. if (a.kind === "record") return b.kind === "record" && a.shapeId === b.shapeId; @@ -798,7 +812,7 @@ export function isRefCounted(t: IrType): boolean { export interface IrModule { /** Bumped on any breaking IR change; serialize.ts refuses mismatches. */ - irVersion: 10; + irVersion: 11; sourceFile: string; functions: IrFunction[]; /** Class shapes. Constructors and methods are ordinary module functions @@ -4443,6 +4457,8 @@ export type IrExpr = * for every program that held its numbers. */ | { kind: "numLit"; value: number; spelling?: string; type: IrType; loc: SrcLoc } | { kind: "strLit"; value: string; type: IrType; loc: SrcLoc } + /** The singleton namespace token for one compiled or builtin module. */ + | { kind: "moduleNsRef"; moduleId: string; type: IrType; loc: SrcLoc } | { kind: "boolLit"; value: boolean; type: IrType; loc: SrcLoc } /** An `undefined` or `null` literal; `type` is the matching unit kind. * Valid ONLY as the immediate value of a `unionWrap` (the frontend's slot @@ -5632,6 +5648,7 @@ function isJsonSafeAt( case "caught": case "promise": case "generator": + case "moduleNs": case "void": return false; // Record fields drop undefined. Stringification also represents it in diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index e0c49330b..057da3e1e 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -6,7 +6,7 @@ import { InternalCompilerError } from "../errors.js"; */ import type { IrModule } from "./ir.js"; -export const IR_VERSION = 10 as const; +export const IR_VERSION = 11 as const; export function serializeModule(mod: IrModule): string { return JSON.stringify(mod, (_key, value) => { diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 27c05ec9d..f5f69d45c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -1908,6 +1908,11 @@ function validateFunction( case "strLit": if (e.type.kind !== "string") err("strLit must be string", e.loc); break; + case "moduleNsRef": + if (e.type.kind !== "moduleNs" || e.type.moduleId !== e.moduleId) { + err("moduleNsRef must match its moduleNs type", e.loc); + } + break; case "boolLit": if (e.type.kind !== "bool") err("boolLit must be bool", e.loc); break; @@ -1942,6 +1947,10 @@ function validateFunction( if (e.right.type.kind !== "classval") { err(`bin ${e.op} on class values: right operand is ${e.right.type.kind}`, e.loc); } + } else if (isEq && e.left.type.kind === "moduleNs") { + if (e.right.type.kind !== "moduleNs") { + err(`bin ${e.op} on module namespaces: right operand is ${e.right.type.kind}`, e.loc); + } } else if ( isEq && (e.left.type.kind === "array" || diff --git a/packages/compiler/src/library/int-infer.test.ts b/packages/compiler/src/library/int-infer.test.ts index d124f3fe2..a843a31a3 100644 --- a/packages/compiler/src/library/int-infer.test.ts +++ b/packages/compiler/src/library/int-infer.test.ts @@ -65,7 +65,7 @@ const sink = (name: string): IrFunction => ({ /** A module holding the case function plus the two declared sinks. */ function caseModule(params: string[], locals: string[], body: IrStmt[]): IrModule { return { - irVersion: 10, + irVersion: 11, sourceFile: "corpus.ts", functions: [ sink("send"), @@ -136,7 +136,7 @@ const RECORD_CFG: IntSlotConfig = { function recordCase(body: IrStmt[], names = ["m"], extraFns: IrFunction[] = []): IrModule { return { - irVersion: 10, + irVersion: 11, sourceFile: "fields.ts", functions: [ ...extraFns, @@ -177,7 +177,7 @@ const classCountRead = (): IrExpr => ({ function onlyOrdinaryClass(body: IrStmt[]): IntVerdict { const mod: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "class-fields.ts", functions: [ sink("send"), @@ -391,7 +391,7 @@ describe("the domain's edges beyond the corpus", () => { loc, }; const mod: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "optional.ts", functions: [{ name: "normalize", diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index dee881d80..0c124caa9 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -250,7 +250,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit", const sourceBefore = await readFile(f.source, "utf8"); const returnStart = sourceBefore.indexOf("return"); const semanticMod = { - irVersion: 10, + irVersion: 11, sourceFile: f.source, functions: [{ name: "__main", @@ -308,7 +308,7 @@ test("semantic library cache refuses token and directive edits", async () => { const f = await fixture(); const sourceBefore = await readFile(f.source, "utf8"); const semanticMod = { - irVersion: 10, + irVersion: 11, sourceFile: f.source, functions: [{ name: "__main", @@ -353,7 +353,7 @@ test("semantic C cache accepts only line-preserving single-source edits", async const f = await fixture(); const sourceBefore = await readFile(f.source, "utf8"); const semanticMod = { - irVersion: 10, + irVersion: 11, sourceFile: f.source, functions: [{ name: "__main", @@ -424,7 +424,7 @@ test("semantic C cache refuses non-LF separator normalization", async () => { frontend: tracker.snapshot(), semantic: { mod: { - irVersion: 10, + irVersion: 11, sourceFile: f.source, functions: [], entry: "__main", @@ -445,7 +445,7 @@ test("semantic C cache refuses comment-only edits in multi-source graphs", async const importedSource = "export function helper(): number { return 1; }\n"; await writeFile(imported, importedSource); const semanticMod = { - irVersion: 10, + irVersion: 11, sourceFile: f.source, functions: [{ name: "__main", diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 7e4ae2221..db8d8f1b5 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -66,7 +66,7 @@ function fixture(): IrModule { ); return { - irVersion: 10, + irVersion: 11, sourceFile: loc.file, entry: "__main", functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }], @@ -136,7 +136,7 @@ function receiverReassignmentFixture(): IrModule { ]; return { - irVersion: 10, + irVersion: 11, sourceFile: loc.file, entry: "__main", functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }], @@ -201,7 +201,7 @@ function integerLoopFixture(mutatesIndex = false): IrModule { { kind: "bytesSet", arr: bytesRef(), index: indexRef(), value: ref("sum"), loc }, ); return { - irVersion: 10, + irVersion: 11, sourceFile: loc.file, entry: "__main", functions: [{ @@ -328,7 +328,7 @@ test("large record clones stay outlined while small clones remain inlineable", ( const fields = Array.from({ length: count }, (_, i) => ({ name: `f${i}`, type: F64 })); const type = { kind: "record", shapeId: id } as const; return { - irVersion: 10, + irVersion: 11, sourceFile: "record-clone.ts", entry: "__main", records: [{ id, fields }], diff --git a/packages/compiler/test/emit-c.test.ts b/packages/compiler/test/emit-c.test.ts index c25cbb93b..89ebcbf48 100644 --- a/packages/compiler/test/emit-c.test.ts +++ b/packages/compiler/test/emit-c.test.ts @@ -35,7 +35,7 @@ test("strings: literals, concat in a loop, toString, RC-clean under audit", asyn // while (i < 3) { acc = acc + ("-" + i); i = i + 1; } // console.log(acc, acc === "x-0-1-2", "α∂" < "β"); const mod: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "s.ts", entry: "__main", functions: [ @@ -113,7 +113,7 @@ test("short-circuit: right operand of && only evaluates when left is true", asyn // if (false && sideEffect()) {} ; if (true || sideEffect()) {} // console.log("done") const mod: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "l.ts", entry: "__main", functions: [ @@ -161,7 +161,7 @@ test("string params: callee owns and releases; returns transfer ownership", asyn // function greet(who: string): string { return "hi " + who; } // console.log(greet("world")); const mod: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "p.ts", entry: "__main", functions: [ diff --git a/packages/compiler/test/fixtures/fib-ir.ts b/packages/compiler/test/fixtures/fib-ir.ts index 2c6f683ce..6425e317c 100644 --- a/packages/compiler/test/fixtures/fib-ir.ts +++ b/packages/compiler/test/fixtures/fib-ir.ts @@ -18,7 +18,7 @@ const n = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, lo const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); export const fibModule: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "fib.ts", entry: "__main", functions: [ diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index d898cc1f1..efd366500 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -22,7 +22,7 @@ test("fib module JSON round-trips", () => { test("validator rejects type mismatches and bad references", () => { const loc = { file: "t.ts", start: 0, end: 0 }; const bad: IrModule = { - irVersion: 10, + irVersion: 11, sourceFile: "t.ts", entry: "__main", functions: [ @@ -83,7 +83,7 @@ test("serializer round-trips ±Infinity and refuses NaN", () => { }); test("deserializer rejects the previous IR version", () => { - const json = serializeModule(fibModule).replace('"irVersion": 10', '"irVersion": 9'); + const json = serializeModule(fibModule).replace('"irVersion": 11', '"irVersion": 10'); expect(() => deserializeModule(json)).toThrow(/version mismatch/); }); diff --git a/packages/compiler/test/llvm-lib-dispatch.test.ts b/packages/compiler/test/llvm-lib-dispatch.test.ts index 509ae61ca..8a90c5f4c 100644 --- a/packages/compiler/test/llvm-lib-dispatch.test.ts +++ b/packages/compiler/test/llvm-lib-dispatch.test.ts @@ -19,7 +19,7 @@ const dyn = (): IrExpr => ({ kind: "dynFrom", value: str("value"), type: DYN, lo function libCallModule(fn: IrLibFn, args: IrExpr[], type: IrType): IrModule { return { - irVersion: 10, + irVersion: 11, sourceFile: loc.file, entry: "__main", functions: [{ diff --git a/packages/compiler/test/string-accumulation-emission.test.ts b/packages/compiler/test/string-accumulation-emission.test.ts index a6e141a83..fc3ff40f5 100644 --- a/packages/compiler/test/string-accumulation-emission.test.ts +++ b/packages/compiler/test/string-accumulation-emission.test.ts @@ -58,7 +58,7 @@ function fixture(): IrModule { { kind: "varDecl", localId: "other", init: str("other"), loc }, ); return { - irVersion: 10, + irVersion: 11, sourceFile: loc.file, entry: "__main", globals: [{ id: "%g.e.acc", name: "globalAccumulator", type: STRING, mutable: true }], diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 87c449c0b..235ac64f5 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -6407,6 +6407,25 @@ ], "diags": [] }, + "/tests/corpus/2933-static-dynamic-import/main.ts": { + "order": [ + "/tests/corpus/2933-static-dynamic-import/main.ts" + ], + "diags": [] + }, + "/tests/corpus/2934-static-import-stored.ts": { + "order": [ + "/tests/corpus/2934-static-import-stored.ts" + ], + "diags": [] + }, + "/tests/corpus/2935-module-namespace-values/main.ts": { + "order": [ + "/tests/corpus/2935-module-namespace-values/mod.ts", + "/tests/corpus/2935-module-namespace-values/main.ts" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" diff --git a/tests/corpus/2052-dynamic-import-self.ts b/tests/corpus/2052-dynamic-import-self.ts index 5608ca350..4886f6965 100644 --- a/tests/corpus/2052-dynamic-import-self.ts +++ b/tests/corpus/2052-dynamic-import-self.ts @@ -1,5 +1,4 @@ -// @dynamic -// Self-import: import() of the ENTRY module answers the entry's own +// Static self-import: import() of the ENTRY module answers the entry's own // exports (no re-evaluation — the module is already running); the // namespace resolves after the entry's synchronous body, like Node. export function tag(): string { return "self"; } diff --git a/tests/corpus/2652-top-level-await-dynamic/main.ts b/tests/corpus/2652-top-level-await-dynamic/main.ts index 7bd6da4fa..e344caf06 100644 --- a/tests/corpus/2652-top-level-await-dynamic/main.ts +++ b/tests/corpus/2652-top-level-await-dynamic/main.ts @@ -1,4 +1,3 @@ -// @dynamic console.log("before dynamic"); const late = await import("./late.ts"); console.log("dynamic", late.late); diff --git a/tests/corpus/2933-static-dynamic-import/main.ts b/tests/corpus/2933-static-dynamic-import/main.ts new file mode 100644 index 000000000..15f2859cf --- /dev/null +++ b/tests/corpus/2933-static-dynamic-import/main.ts @@ -0,0 +1,31 @@ +// Engine-free literal import(): compiled ESM and Node builtin namespaces +// resolve on the native microtask queue, preserve one namespace identity, +// and keep exported mutable bindings live across repeated imports. +console.log("start"); + +const firstPromise = import("./mod.ts"); +const secondPromise = import("./mod.ts"); +console.log("sync tail"); + +async function main(): Promise { + const first = await firstPromise; + const second = await secondPromise; + console.log("identity", first === second); + console.log("initial", first.count, second.count); + first.bump(); + console.log("live", first.count, second.count); + console.log("class", new first.Box(7).value()); + console.log("default", first.default); + console.log("keys", Object.keys(first).join(",")); + + const viaThen = await import("./mod.ts").then((ns) => ns.label()); + console.log("then", viaThen); + + const path = await import("node:path"); + const pathAgain = await import("node:path"); + console.log("builtin identity", path === pathAgain); + console.log("builtin", path.join("a", "b"), path.sep.length > 0, typeof path); + console.log("builtin then", await import("node:path").then((ns) => ns.basename("a/b.txt"))); +} + +main(); diff --git a/tests/corpus/2933-static-dynamic-import/mod.ts b/tests/corpus/2933-static-dynamic-import/mod.ts new file mode 100644 index 000000000..b40396bfb --- /dev/null +++ b/tests/corpus/2933-static-dynamic-import/mod.ts @@ -0,0 +1,25 @@ +export let count = 0; + +export function bump(): void { + count += 1; +} + +export function label(): string { + return "module-label"; +} + +export class Box { + private readonly n: number; + + constructor(n: number) { + this.n = n; + } + + value(): number { + return this.n; + } +} + +export default "module-default"; + +console.log("module evaluated"); diff --git a/tests/corpus/2934-static-import-stored.ts b/tests/corpus/2934-static-import-stored.ts new file mode 100644 index 000000000..1f87b6cd1 --- /dev/null +++ b/tests/corpus/2934-static-import-stored.ts @@ -0,0 +1,15 @@ +// A module-namespace promise stored at file scope retains its native +// Promise ABI even when @types/node exposes the builtin as an +// anonymous object type. +import * as declaredPath from "node:path"; + +const pathPromise = import("node:path"); + +async function main(): Promise { + const path = await pathPromise; + console.log(declaredPath === path); + console.log(path.basename("one/two.txt")); + console.log(await pathPromise.then((ns) => ns.dirname("one/two.txt"))); +} + +main(); diff --git a/tests/corpus/2935-module-namespace-values/main.ts b/tests/corpus/2935-module-namespace-values/main.ts new file mode 100644 index 000000000..6b5e86fda --- /dev/null +++ b/tests/corpus/2935-module-namespace-values/main.ts @@ -0,0 +1,16 @@ +// Namespace-import objects and dynamic import() share the same singleton +// identity, while qualified reads stay live aliases of exporter storage. +import * as declared from "./mod.ts"; + +const alias = declared; + +async function main(): Promise { + const dynamic = await import("./mod.ts"); + console.log("identity", alias === declared, declared === dynamic); + console.log("keys", Object.keys(alias).join(",")); + console.log("value", alias.value); + dynamic.bump(); + console.log("live", alias.value, declared.value, dynamic.value); +} + +main(); diff --git a/tests/corpus/2935-module-namespace-values/mod.ts b/tests/corpus/2935-module-namespace-values/mod.ts new file mode 100644 index 000000000..38148d97c --- /dev/null +++ b/tests/corpus/2935-module-namespace-values/mod.ts @@ -0,0 +1,5 @@ +export let value = 1; + +export function bump(): void { + value += 1; +} diff --git a/tests/diagnostics/dynamic-import-static.ts b/tests/diagnostics/dynamic-import-static.ts index 57323d0ee..72524f005 100644 --- a/tests/diagnostics/dynamic-import-static.ts +++ b/tests/diagnostics/dynamic-import-static.ts @@ -1,10 +1,8 @@ -// Dynamic import() in a STATIC build: every site is its own SC2012 — the -// module's execution home is the embedded engine, so the honest story is -// the requires---dynamic diagnostic, per site, poison-recovered. -async function run(): Promise { - const fs = await import("fs"); - console.log(fs ? "y" : "n"); - const p = await import("node:path"); - console.log(p ? "y" : "n"); +// Static import() embeds a fixed graph, so a runtime-computed specifier +// remains an explicit refusal even though literal program/builtin modules +// now compile without the dynamic engine. +async function run(specifier: string): Promise { + const module = await import(specifier); + console.log(module); } -run(); +run("node:path"); diff --git a/tests/diagnostics/ns-import-object/main.ts b/tests/diagnostics/ns-import-object/main.ts index ab830a691..531686955 100644 --- a/tests/diagnostics/ns-import-object/main.ts +++ b/tests/diagnostics/ns-import-object/main.ts @@ -1,9 +1,10 @@ -// The module namespace OBJECT as a first-class value: member accesses -// resolve statically (lib.one() compiles), but Node's frozen, -// alphabetically-keyed namespace object is not materialized — storing, -// passing, or iterating it fences by name. +// Module namespace objects are first-class nominal values and their member +// reads stay live. Object.values remains fenced because materializing a +// heterogeneous live-value array has no sound static representation. import * as lib from "./lib.ts"; console.log(lib.one()); const grabbed = lib; console.log(grabbed.one()); +console.log(Object.values(grabbed)); +// Namespace value enumeration remains explicit. diff --git a/tests/harness/__snapshots__/dynamic-import-static.ts.txt b/tests/harness/__snapshots__/dynamic-import-static.ts.txt index 028f8e6a8..dbc2f00bb 100644 --- a/tests/harness/__snapshots__/dynamic-import-static.ts.txt +++ b/tests/harness/__snapshots__/dynamic-import-static.ts.txt @@ -1,35 +1,15 @@ -dynamic-import-static.ts:5:20 - error SC2012: 'import()' runs in the embedded dynamic engine, which this build does not include +dynamic-import-static.ts:5:24 - error SC1090: dynamic import() of computed specifiers (the module graph embeds at build time — the specifier must be a string literal) are not supported yet - 4 | async function run(): Promise { - 5 | const fs = await import("fs"); - | ^~~~~~~~~~~~ - 6 | console.log(fs ? "y" : "n"); + 4 | async function run(specifier: string): Promise { + 5 | const module = await import(specifier); + | ^~~~~~~~~~~~~~~~~ + 6 | console.log(module); - hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it +dynamic-import-static.ts:6:15 - error SC2004: uses of 'module' inherit the blocker on its declaration -dynamic-import-static.ts:6:15 - error SC2004: uses of 'fs' inherit the blocker on its declaration + 5 | const module = await import(specifier); + 6 | console.log(module); + | ^~~~~~ + 7 | } - 5 | const fs = await import("fs"); - 6 | console.log(fs ? "y" : "n"); - | ^~ - 7 | const p = await import("node:path"); - - hint: the declaration of 'fs' did not compile — fix the diagnostic reported there and these sites clear with it - -dynamic-import-static.ts:7:19 - error SC2012: 'import()' runs in the embedded dynamic engine, which this build does not include - - 6 | console.log(fs ? "y" : "n"); - 7 | const p = await import("node:path"); - | ^~~~~~~~~~~~~~~~~~~ - 8 | console.log(p ? "y" : "n"); - - hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it - -dynamic-import-static.ts:8:15 - error SC2004: uses of 'p' inherit the blocker on its declaration - - 7 | const p = await import("node:path"); - 8 | console.log(p ? "y" : "n"); - | ^ - 9 | } - - hint: the declaration of 'p' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file + hint: the declaration of 'module' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/ns-import-object/main.ts.txt b/tests/harness/__snapshots__/ns-import-object/main.ts.txt index bd69d8b62..5bfe65442 100644 --- a/tests/harness/__snapshots__/ns-import-object/main.ts.txt +++ b/tests/harness/__snapshots__/ns-import-object/main.ts.txt @@ -1,6 +1,8 @@ -ns-import-object/main.ts:8:17 - error SC1013: module namespace objects as first-class values (access 'lib' members directly: lib.) are not supported yet +ns-import-object/main.ts:9:13 - error SC2020: 'Object.values over a module namespace' is part of the standard library types but has no scriptc lowering yet - 7 | console.log(lib.one()); - 8 | const grabbed = lib; - | ^~~ - 9 | console.log(grabbed.one()); \ No newline at end of file + 8 | console.log(grabbed.one()); + 9 | console.log(Object.values(grabbed)); + | ^~~~~~~~~~~~~~~~~~~~~~ + 10 | // Namespace value enumeration remains explicit. + + hint: read the named exports directly (module namespace values are live and may have heterogeneous representations) \ No newline at end of file