diff --git a/docs/src/app/limitations/page.mdx b/docs/src/app/limitations/page.mdx index 324be7b51..38ce098d6 100644 --- a/docs/src/app/limitations/page.mdx +++ b/docs/src/app/limitations/page.mdx @@ -12,7 +12,7 @@ These are rejected at compile time with an `SC` code, a code frame, and usually - Block- and function-scoped `using` and `await using` compile for statically represented classes with zero-parameter disposal methods and for the supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops compile. Top-level and switch-clause declarations, disposal through an engine-held package value, and the `DisposableStack` constructors remain fenced; a caught `SuppressedError` exposes its Node-compatible `name` and `message`, while its `error` and `suppressed` payload properties remain outside the static catch surface. - Typed async generator functions and methods compile with lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also consumes Node `Readable` streams, Web `ReadableStream` values, and statically represented class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record; abrupt completion runs the iterator's cleanup, including literal `destroyOnReturn` and `preventCancel` options. Async `yield*` delegation, stored Web/Node iterator handles, and other structural async-iterator objects remain fenced. The `node:timers/promises` `setInterval(delay, value)` async iterator compiles, while its AbortSignal options remain fenced. - 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. Immutable aliases of table-backed Node builtins can be called directly, while escaping builtin values such as `array.map(path.basename)` remain fenced; `util.promisify` has compile-time projections for `child_process.execFile` and utf8 `fs.readFile`. +- 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. Exact-arity `node:path` functions such as `extname`, `isAbsolute`, and `relative` are first-class static values and can be stored, returned, and passed to higher-order functions; optional and variadic builtins such as `basename`, `join`, and `resolve` remain call-only. 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. **Types and shapes** diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index db9136d11..9b4a6bb2c 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -65977,7 +65977,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.basename", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66014,7 +66016,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.dirname", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66051,7 +66055,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.extname", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66156,7 +66162,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.isAbsolute", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66193,7 +66201,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.join", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66230,7 +66240,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.normalize", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66302,7 +66314,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.relative", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66339,7 +66353,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.resolve", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66376,7 +66392,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.toNamespacedPath", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66413,7 +66431,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.delimiter", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66450,7 +66470,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.posix", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66487,7 +66509,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.sep", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -66524,7 +66548,9 @@ "confidence": "high", "source": "surface-manifest:node-builtin.path.win32", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 444a31c06..46e8989b0 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -82352,6 +82352,8 @@ "status": "partial", "evidence": "derived:descendants", "tests": [ + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts", "tests/corpus/957-builtins-namespace.ts" ] }, @@ -82412,7 +82414,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.basename", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82445,7 +82449,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.dirname", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82478,7 +82484,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.extname", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82568,7 +82576,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.isAbsolute", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82601,7 +82611,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.join", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82634,7 +82646,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.normalize", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82697,7 +82711,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.relative", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82730,7 +82746,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.resolve", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82763,7 +82781,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.toNamespacedPath", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82796,7 +82816,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.delimiter", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82829,7 +82851,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.posix", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82862,7 +82886,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.sep", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { @@ -82895,7 +82921,9 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.path.win32", "tests": [ - "tests/corpus/957-builtins-namespace.ts" + "tests/corpus/957-builtins-namespace.ts", + "tests/corpus/2915-path-function-values.ts", + "tests/corpus/2916-path-function-values-cjs.ts" ] }, "dynamic": { diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 217b39aee..e6c58f4c2 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -51,7 +51,7 @@ "node-builtin.module": ["tests/corpus/1598-cjs-builtin-require/main.js"], "node-builtin.net": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"], "node-builtin.os": ["tests/corpus/1480-os-network-interfaces.ts"], - "node-builtin.path": ["tests/corpus/957-builtins-namespace.ts"], + "node-builtin.path": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2915-path-function-values.ts", "tests/corpus/2916-path-function-values-cjs.ts"], "node-builtin.perf_hooks": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"], "node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts"], diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index 18076a9a4..66b99ea01 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -911,6 +911,20 @@ function arraySearchHelper( } lowerer.implicitParamTypes = previousImplicit; } + // Array storage widens indexed reads with undefined so holes remain + // observable. The HOF helper guards every callback behind arrayHas, + // so an exact-ABI builtin closure may safely adapt from its declared + // parameter to that guarded union. Keep this exception scoped to the + // compiler-generated builtin values; ordinary callback-value policy is + // unchanged. + if ( + fnArg.type.kind === "func" && lowerer.isBuiltinCallableValue(fnArg) + ) { + const callbackType = funcOf(full.slice(0, fnArg.type.params.length), fnArg.type.ret); + if (!typeEquals(fnArg.type, callbackType)) { + fnArg = lowerer.coerceToExpected(fnArg, callbackType); + } + } if ( fnArg.type.kind !== "func" || fnArg.type.params.length > full.length || diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 16f23d29d..d13646008 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -728,6 +728,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ); } if (projection?.kind === "builtin-function") { + if (!isJsSourceFile(expr.getSourceFile())) { + const callable = lowerer.lowerBuiltinCallableValue(projection, loc); + if (callable) return callable; + } lowerer.unsupported( "SC1090", expr, @@ -950,6 +954,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (isJsSourceFile(expr.getSourceFile())) { return { kind: "strLit", value: `[builtin ${bi.module}.${bi.member}]`, type: STRING, loc }; } + { + const callable = lowerer.lowerBuiltinCallableValue(bi, loc); + if (callable) return callable; + } if (builtinModuleFnOf(lowerer, bi.module, bi.member)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index cd7c2d830..13cfaf99e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1537,6 +1537,11 @@ export class Lowerer { * here by name so every reference is the SAME zero-capture closure and * `opt.type === String` is JS identity (see primitiveCtorClosure). */ readonly primitiveCtorFns = new Map(); + /** Builtin-module FUNCTIONS admitted as values by their surface-table + * entries. The resolved runtime function plus ABI is the identity key, + * so named imports, namespace reads, CommonJS bindings, and alias chains + * all reuse one immortal zero-capture closure. */ + readonly builtinCallableValueFns = new Map(); /** Optional-chain lowering state. While a chain body lowers, the guarded * receiver NODE reads as a chainRecv (typed by the narrowed arm) instead * of re-lowering, its checker type reads non-nullish (typeOf), and the @@ -9938,6 +9943,13 @@ export class Lowerer { const roots = lowerTlsRootCertificates(this, bi, loc); if (roots) return roots; } + // TypeScript builtin values admitted by an explicit surface-table + // contract materialize as interned closures. JavaScript sources retain + // their established builtin identity-token/fence policy. + if (!isJsSourceFile(expr.getSourceFile())) { + const callable = this.lowerBuiltinCallableValue(bi, loc); + if (callable) return callable; + } if (bi.member === "constants" && bi.module === "fs") { // A bare `fs.constants` read (not one of the baked bits above). this.noLowering(`fs.constants`, expr, "F_OK, R_OK, W_OK, and X_OK are the lowered constants"); @@ -9957,6 +9969,68 @@ export class Lowerer { ); } + /** Materialize one explicitly value-callable table entry as an interned + * synthetic module function. Direct calls never use this path: they keep + * their existing validated libCall lowering. The descriptor opt-in is + * deliberately narrower than table membership because many table rows + * are only dispatch sentinels for call-site-specific lowering. */ + lowerBuiltinCallableValue( + bi: { module: string; member: string }, + loc: SrcLoc, + ): IrExpr | null { + const fn = builtinModuleFnOf(this, bi.module, bi.member); + if (fn?.valueCallable !== true) return null; + if (fn.variadicPack || fn.defaults !== undefined) { + throw new InternalCompilerError( + `builtin callable value '${bi.module}.${bi.member}' has a variable-width descriptor`, + ); + } + const funcType = { kind: "func" as const, params: fn.params, ret: fn.result }; + const key = `${fn.fn}:${typeKey(funcType)}`; + let fnName = this.builtinCallableValueFns.get(key); + if (!fnName) { + fnName = `%builtin.value.${fn.fn}.${this.builtinCallableValueFns.size}`; + this.builtinCallableValueFns.set(key, fnName); + const params = fn.params.map((type, index) => ({ + localId: `arg.${index}`, + name: `arg${index}`, + type, + })); + const args: IrExpr[] = params.map((param) => ({ + kind: "varRef", + localId: param.localId, + type: param.type, + loc, + })); + const call: IrExpr = { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; + const body: IrStmt[] = fn.result.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }]; + this.liftedFns.push({ + name: fnName, + params, + returnType: fn.result, + locals: params.map((param) => ({ + id: param.localId, + name: param.name, + type: param.type, + mutable: false, + })), + body, + loc, + }); + } + return { kind: "closure", fnName, captures: [], type: funcType, loc }; + } + + isBuiltinCallableValue(expr: IrExpr): boolean { + return expr.kind === "closure" && + [...this.builtinCallableValueFns.values()].some((name) => name === expr.fnName); + } + lowerBuiltinModuleCall(expr: ts.CallExpression, bi: { module: string; member: string }, fn: BuiltinModuleFn, diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 9499e8b52..ac3cfefdc 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -569,6 +569,11 @@ export interface BuiltinModuleFn { result: IrType; variadicPack?: boolean; defaults?: string[]; + /** This exact fixed-width table entry may materialize as an interned + * zero-capture closure when the builtin escapes call position. Entries + * with call-site-specific validation, optional completion, or rest + * packing stay call-only until they have an equally exact adapter. */ + valueCallable?: true; } /** The lowerable surface of the supported node builtin modules, keyed by @@ -586,13 +591,13 @@ export interface BuiltinModuleFn { const PATH_MODULE_FNS: Record = { join: { fn: "path.join", params: [STRING], result: STRING, variadicPack: true }, resolve: { fn: "path.resolve", params: [STRING], result: STRING, variadicPack: true }, - normalize: { fn: "path.normalize", params: [STRING], result: STRING }, - dirname: { fn: "path.dirname", params: [STRING], result: STRING }, + normalize: { fn: "path.normalize", params: [STRING], result: STRING, valueCallable: true }, + dirname: { fn: "path.dirname", params: [STRING], result: STRING, valueCallable: true }, basename: { fn: "path.basename", params: [STRING, STRING], result: STRING, defaults: [""] }, - extname: { fn: "path.extname", params: [STRING], result: STRING }, - isAbsolute: { fn: "path.isAbsolute", params: [STRING], result: BOOL }, - relative: { fn: "path.relative", params: [STRING, STRING], result: STRING }, - toNamespacedPath: { fn: "path.toNamespacedPath", params: [STRING], result: STRING }, + extname: { fn: "path.extname", params: [STRING], result: STRING, valueCallable: true }, + isAbsolute: { fn: "path.isAbsolute", params: [STRING], result: BOOL, valueCallable: true }, + relative: { fn: "path.relative", params: [STRING, STRING], result: STRING, valueCallable: true }, + toNamespacedPath: { fn: "path.toNamespacedPath", params: [STRING], result: STRING, valueCallable: true }, }; /** The win32 twins (scr_path.c's Node-v24 path.win32 port, byte-for-byte): @@ -601,13 +606,13 @@ const PATH_MODULE_FNS: Record = { const PATH_WIN32_MODULE_FNS: Record = { join: { fn: "path.win32Join", params: [STRING], result: STRING, variadicPack: true }, resolve: { fn: "path.win32Resolve", params: [STRING], result: STRING, variadicPack: true }, - normalize: { fn: "path.win32Normalize", params: [STRING], result: STRING }, - dirname: { fn: "path.win32Dirname", params: [STRING], result: STRING }, + normalize: { fn: "path.win32Normalize", params: [STRING], result: STRING, valueCallable: true }, + dirname: { fn: "path.win32Dirname", params: [STRING], result: STRING, valueCallable: true }, basename: { fn: "path.win32Basename", params: [STRING, STRING], result: STRING, defaults: [""] }, - extname: { fn: "path.win32Extname", params: [STRING], result: STRING }, - isAbsolute: { fn: "path.win32IsAbsolute", params: [STRING], result: BOOL }, - relative: { fn: "path.win32Relative", params: [STRING, STRING], result: STRING }, - toNamespacedPath: { fn: "path.win32ToNamespacedPath", params: [STRING], result: STRING }, + extname: { fn: "path.win32Extname", params: [STRING], result: STRING, valueCallable: true }, + isAbsolute: { fn: "path.win32IsAbsolute", params: [STRING], result: BOOL, valueCallable: true }, + relative: { fn: "path.win32Relative", params: [STRING, STRING], result: STRING, valueCallable: true }, + toNamespacedPath: { fn: "path.win32ToNamespacedPath", params: [STRING], result: STRING, valueCallable: true }, }; export const BUILTIN_MODULE_FNS: Record | undefined> = { diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 2c621ea90..62937fdd8 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -6331,6 +6331,18 @@ ], "diags": [] }, + "/tests/corpus/2915-path-function-values.ts": { + "order": [ + "/tests/corpus/2915-path-function-values.ts" + ], + "diags": [] + }, + "/tests/corpus/2916-path-function-values-cjs.ts": { + "order": [ + "/tests/corpus/2916-path-function-values-cjs.ts" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" diff --git a/tests/corpus/2915-path-function-values.ts b/tests/corpus/2915-path-function-values.ts new file mode 100644 index 000000000..3465a27d5 --- /dev/null +++ b/tests/corpus/2915-path-function-values.ts @@ -0,0 +1,57 @@ +// Exact-arity node:path functions are first-class static values. Every +// import spelling shares one interned closure per runtime function, values +// flow through containers and user callbacks, and array HOFs call them with +// the declared prefix of JavaScript's callback arguments. +import { dirname, extname, isAbsolute, normalize, relative, toNamespacedPath } from "node:path"; +import * as path from "node:path"; +import * as posix from "node:path/posix"; +import * as win32 from "node:path/win32"; + +const ext = extname; +const extAgain = ext; +const paths = ["/tmp/a.ts", "relative.js", "/root/readme"]; +console.log(paths.map(extname).join("|")); +console.log(paths.filter(isAbsolute).join("|")); +console.log(ext === extname, extAgain === path.extname, typeof ext); + +function apply1(fn: (value: string) => string, value: string): string { + return fn(value); +} + +function apply2(fn: (left: string, right: string) => string, left: string, right: string): string { + return fn(left, right); +} + +console.log(apply1(dirname, "/a/b/c.ts")); +console.log(apply2(relative, "/a/b", "/a/c/d")); +console.log(apply1(toNamespacedPath, "/tmp/example")); + +const stored: ((value: string) => string)[] = [normalize, dirname, extname]; +console.log(stored.map((fn) => fn("a//b/../file.ts")).join("|")); + +let mutable: (value: string) => string = extname; +console.log(mutable("archive.tar.gz")); +mutable = dirname; +console.log(mutable("/one/two/three.txt")); + +const operations: { + normalize: (value: string) => string; + extname: (value: string) => string; +} = { normalize: path.normalize, extname: path.extname }; +console.log(operations.normalize("a//b/../c"), operations.extname("index.test.ts")); + +function choose(flag: boolean): (value: string) => string { + return flag ? normalize : dirname; +} + +function wrap(fn: (value: string) => string): (value: string) => string { + return (value) => fn(value); +} + +console.log(choose(true)("a//b/../c"), choose(false)("/a/b/c")); +console.log(wrap(extname)("wrapped.test.ts")); + +console.log(path.extname === extname); +console.log(path.posix.extname === posix.extname, path.win32.extname === win32.extname); +console.log(apply1(path.posix.normalize, "a//b/../c")); +console.log(apply2(win32.relative, "C:\\a\\b", "C:\\a\\c\\d")); diff --git a/tests/corpus/2916-path-function-values-cjs.ts b/tests/corpus/2916-path-function-values-cjs.ts new file mode 100644 index 000000000..f4e8cff85 --- /dev/null +++ b/tests/corpus/2916-path-function-values-cjs.ts @@ -0,0 +1,15 @@ +// The CommonJS namespace and destructured require spellings materialize +// the same exact-arity node:path callable values as ESM imports. +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const path = require("node:path") as typeof import("node:path"); +const { extname, isAbsolute, normalize } = require("node:path") as typeof import("node:path"); + +const paths = ["/tmp/a.ts", "relative.js", "/root/readme"]; +console.log(paths.map(extname).join("|")); +console.log(paths.filter(isAbsolute).join("|")); +console.log(path.extname === extname, path.normalize === normalize); + +const transforms: ((value: string) => string)[] = [path.normalize, path.dirname, extname]; +console.log(transforms.map((fn) => fn("a//b/../file.ts")).join("|")); diff --git a/tests/coverage-fixtures/js-builtin-callable-alias.js b/tests/coverage-fixtures/js-builtin-callable-alias.js new file mode 100644 index 000000000..ff31ffda2 --- /dev/null +++ b/tests/coverage-fixtures/js-builtin-callable-alias.js @@ -0,0 +1,8 @@ +// JavaScript builtin aliases remain compile-time call targets, not static +// function values. The direct call lowers; the escaping value keeps the +// pre-existing named fence instead of materializing a TypeScript closure. +import { extname } from "node:path"; + +const ext = extname; +console.log(ext("/tmp/example.ts")); +console.log(ext); diff --git a/tests/diagnostics/stdlib.ts b/tests/diagnostics/stdlib.ts index 7daac54d6..5f13f7655 100644 --- a/tests/diagnostics/stdlib.ts +++ b/tests/diagnostics/stdlib.ts @@ -1,11 +1,14 @@ // tsc-clean misuses of the standard-library surface: each line below is // valid TypeScript against the ambient declarations but outside the -// supported lowering (library, island-backed, Math, string, and number -// functions have no value form; `process` itself is not a first-class -// value). +// supported lowering (most library, island-backed, Math, string, and +// number functions have no value form; exact-arity path functions are the +// explicit exception; `process` itself is not a first-class value). import { readFileSync } from "node:fs"; +import { basename, join } from "node:path"; const read = readFileSync; console.log(read); +const base = basename; console.log(base); +const joiner = join; console.log(joiner); const cwd = process.cwd; const p = process; const env = process.env; diff --git a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt new file mode 100644 index 000000000..2dbfe55a2 --- /dev/null +++ b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt @@ -0,0 +1,7 @@ +scriptc coverage tests/coverage-fixtures/js-builtin-callable-alias.js + + statements analyzed 3 + compile statically 2 (66%) + + deferred to runtime 1 site (JS statements that throw their fence if executed) + ×1 a builtin function alias as an escaping value (call 'ext' directly) SC1090 diff --git a/tests/harness/__snapshots__/stdlib.ts.txt b/tests/harness/__snapshots__/stdlib.ts.txt index d334d17f2..49c9396e0 100644 --- a/tests/harness/__snapshots__/stdlib.ts.txt +++ b/tests/harness/__snapshots__/stdlib.ts.txt @@ -1,41 +1,55 @@ -stdlib.ts:8:40 - error SC1090: a builtin function alias as an escaping value (call 'read' directly) is not supported yet +stdlib.ts:9:40 - error SC1090: a builtin function alias as an escaping value (call 'read' directly) is not supported yet - 7 | - 8 | const read = readFileSync; console.log(read); - | ^~~~ - 9 | const cwd = process.cwd; + 8 | + 9 | const read = readFileSync; console.log(read); + | ^~~~ + 10 | const base = basename; console.log(base); -stdlib.ts:9:13 - error SC1090: process methods as values (call 'cwd' directly) are not supported yet +stdlib.ts:10:36 - error SC1090: a builtin function alias as an escaping value (call 'base' directly) is not supported yet - 8 | const read = readFileSync; console.log(read); - 9 | const cwd = process.cwd; + 9 | const read = readFileSync; console.log(read); + 10 | const base = basename; console.log(base); + | ^~~~ + 11 | const joiner = join; console.log(joiner); + +stdlib.ts:11:34 - error SC1090: a builtin function alias as an escaping value (call 'joiner' directly) is not supported yet + + 10 | const base = basename; console.log(base); + 11 | const joiner = join; console.log(joiner); + | ^~~~~~ + 12 | const cwd = process.cwd; + +stdlib.ts:12:13 - error SC1090: process methods as values (call 'cwd' directly) are not supported yet + + 11 | const joiner = join; console.log(joiner); + 12 | const cwd = process.cwd; | ^~~~~~~~~~~ - 10 | const p = process; + 13 | const p = process; -stdlib.ts:12:13 - error SC1090: Math methods as values (call 'floor' directly) are not supported yet +stdlib.ts:15:13 - error SC1090: Math methods as values (call 'floor' directly) are not supported yet - 11 | const env = process.env; - 12 | const flo = Math.floor; + 14 | const env = process.env; + 15 | const flo = Math.floor; | ^~~~~~~~~~ - 13 | const upper = "abc".toUpperCase; + 16 | const upper = "abc".toUpperCase; -stdlib.ts:13:15 - error SC1090: string methods as values (call 'toUpperCase' directly) are not supported yet +stdlib.ts:16:15 - error SC1090: string methods as values (call 'toUpperCase' directly) are not supported yet - 12 | const flo = Math.floor; - 13 | const upper = "abc".toUpperCase; + 15 | const flo = Math.floor; + 16 | const upper = "abc".toUpperCase; | ^~~~~~~~~~~~~~~~~ - 14 | const fix = (1.5).toFixed; + 17 | const fix = (1.5).toFixed; -stdlib.ts:14:13 - error SC1090: number methods as values (call 'toFixed' directly) are not supported yet +stdlib.ts:17:13 - error SC1090: number methods as values (call 'toFixed' directly) are not supported yet - 13 | const upper = "abc".toUpperCase; - 14 | const fix = (1.5).toFixed; + 16 | const upper = "abc".toUpperCase; + 17 | const fix = (1.5).toFixed; | ^~~~~~~~~~~~~ - 15 | const pf = parseFloat; + 18 | const pf = parseFloat; -stdlib.ts:15:12 - error SC1090: library functions as values (call 'parseFloat' directly) are not supported yet +stdlib.ts:18:12 - error SC1090: library functions as values (call 'parseFloat' directly) are not supported yet - 14 | const fix = (1.5).toFixed; - 15 | const pf = parseFloat; + 17 | const fix = (1.5).toFixed; + 18 | const pf = parseFloat; | ^~~~~~~~~~ - 16 | \ No newline at end of file + 19 | \ No newline at end of file diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 473dd3137..ef03b6c82 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -76,6 +76,12 @@ test("JS inference gaps land where 'any' lands: SC2011 static, island dynamic", ); }); +test("JavaScript builtin aliases keep their deferred value fence", async () => { + await expect(report(fixture("js-builtin-callable-alias.js"))).toMatchFileSnapshot( + "__snapshots__/coverage-js-builtin-callable-alias.txt", + ); +}); + test("any-typed checked-dynamic locals honor the --dynamic coverage promise", () => { const file = join(repoRoot, "tests/corpus/2856-dynamic-any-local-operators.ts"); const staticCoverage = analyze(file).coverage;