From fa98044a5cae785c92374ff0413eeb76c1257539 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:11:52 -0500 Subject: [PATCH 01/45] Run completion callbacks in their call's AsyncLocalStorage context, and add bind, snapshot and store options Node runs the callback of an operation that a callback-style API starts with the AsyncLocalStorage stores active when the call started it. The runtime invokes these callbacks from the event loop, where the context is empty, so getStore() answered undefined in fs, crypto, zlib, dns, child_process, net, http and dgram callbacks. In programs whose scheduling carries contexts (RuntimeFeatures asyncContext), the lowering now binds each completion-callback argument to the context active at the call (completion-callbacks.ts): typed closures through als.bindCallback, whose bound closure enters through a thunk the backend emits once per callback signature, and dyn functions through als.bindCallbackDyn. The fs callback family binds in the runtime, where scr_fs_cb_schedule queues the callback. Other programs compile as before. AsyncLocalStorage.bind(fn) binds fn to the active context, also when it is empty (a typed function keeps its type; in JavaScript files the bound function is named "bound" and forwards its receiver, as in Node). AsyncLocalStorage.snapshot() answers a function that runs its callback in the snapshot's context. new AsyncLocalStorage({ defaultValue, name }) validates its options as Node does; getStore() answers the default value where the store has no entry, exit() runs its callback with the store undefined, and the name property answers the name option or "". Event listeners on sockets, requests, responses and streams, stream callbacks, server.close(callback) and https and tls callbacks still run with no store; the limitations page lists them. Corpus 7110-7112. --- .../builtin-apis/asynclocalstorage.mdx | 4 +- .../generated/node-v24-internal.json | 43 +++++- internal/compatibility/static-support.json | 2 +- .../ambient/scriptc-node-fallback.d.ts | 13 +- .../compiler/src/backend/llvm/lib-process.ts | 52 ++++++- .../compiler/src/backend/llvm/lib-shared.ts | 4 + .../lowering/builtins/async-context.ts | 79 +++++++++- .../frontend/lowering/completion-callbacks.ts | 130 +++++++++++++++++ .../lowering/expressions/property-access.ts | 3 + .../src/frontend/lowering/lower-classes.ts | 23 +-- .../compiler/src/frontend/lowering/lowerer.ts | 2 + packages/compiler/src/frontend/type-mapper.ts | 23 +++ packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 14 ++ packages/compiler/src/ir/runtime-features.ts | 4 + packages/compiler/src/ir/validate.ts | 7 + .../test/completion-callbacks.test.ts | 60 ++++++++ .../7110-als-completion-callbacks.ts.json | 6 + .../7111-als-bind-snapshot-options.ts.json | 6 + .../7112-als-bind-snapshot-dynamic.mjs.json | 6 + packages/runtime/src/scr_async.c | 41 ++++++ packages/runtime/src/scr_async.h | 15 ++ packages/runtime/src/scr_async_dyn.c | 138 +++++++++++++++++- tests/corpus/7110-als-completion-callbacks.ts | 135 +++++++++++++++++ .../corpus/7111-als-bind-snapshot-options.ts | 59 ++++++++ .../corpus/7112-als-bind-snapshot-dynamic.mjs | 51 +++++++ .../7110-als-completion-callbacks.ts.json | 1 + .../7111-als-bind-snapshot-options.ts.json | 1 + .../7112-als-bind-snapshot-dynamic.mjs.json | 1 + 29 files changed, 898 insertions(+), 27 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/completion-callbacks.ts create mode 100644 packages/compiler/test/completion-callbacks.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json create mode 100644 tests/corpus/7110-als-completion-callbacks.ts create mode 100644 tests/corpus/7111-als-bind-snapshot-options.ts create mode 100644 tests/corpus/7112-als-bind-snapshot-dynamic.mjs create mode 100644 tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json create mode 100644 tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json create mode 100644 tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 341994faa..99f89d65c 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -1,2 +1,4 @@ - **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. - Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started. + The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node. + Event listeners on sockets, requests, responses and streams, stream callbacks such as `write(chunk, callback)`, `server.close(callback)`, and the callbacks of `https` and `tls` calls run with no store, where Node runs them in the context of the resource or of the code that triggers them. + `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index d40bb7bfc..34fe2a843 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1387,7 +1387,10 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "chapter" @@ -1437,7 +1440,10 @@ "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1510,7 +1516,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1537,7 +1546,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1564,7 +1576,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1591,7 +1606,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.run", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1618,7 +1636,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1922,6 +1943,9 @@ "tests": [ "tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2484,7 +2508,10 @@ "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 6fc2c563c..efc60f263 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b3d2954a8..ac30fde4a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -6887,9 +6887,18 @@ declare module "node:tty" { declare module "async_hooks" { /* AsyncLocalStorage: fiber-carried context snapshots — run/getStore/ * exit/enterWith/disable lower onto the runtime's active-slot machinery - * (als.* libCalls); values cross as dyn values. */ + * (als.* libCalls); values cross as dyn values. bind and snapshot bind + * functions to the active context; the options carry the store's + * default value and name. */ + export interface AsyncLocalStorageOptions { + defaultValue?: any; + name?: string | undefined; + } export class AsyncLocalStorage { - constructor(); + constructor(options?: AsyncLocalStorageOptions); + static bind any>(fn: Func): Func; + static snapshot(): (fn: (...args: TArgs) => R, ...args: TArgs) => R; + readonly name: string; run(store: T, callback: (...args: any[]) => R, ...args: any[]): R; exit(callback: (...args: any[]) => R, ...args: any[]): R; getStore(): T | undefined; diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 3e8a9bff5..1a3973b07 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -1,10 +1,11 @@ /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { undefinedArmTag } from "../../ir/analysis.js"; -import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js"; +import { F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; import { mockTimerSym } from "./lib-shared.js"; +import { FN_ATTRS } from "./shapes.js"; export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -460,9 +461,58 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr for (const a of e.args) host.emitExpr(a); return { name: "", type: e.type }; } + if (e.fn === "als.bindCallback") { + // The closure moves into the bound closure, whose entry is the + // per-signature thunk; with nothing to bind the runtime answers the + // closure itself. + if (e.type.kind !== "func") + throw new InternalCompilerError("llvm emitter bug: als.bindCallback on non-func"); + const cb = host.emitExpr(e.args[0]!); + const always = host.emitExpr(e.args[1]!); + host.toHeap(cb); + const thunk = boundCallbackThunk(host, e.type); + host.declare("declare ptr @scr_als_bind_closure(ptr, ptr, i1 zeroext)"); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_als_bind_closure(ptr ${cb.name}, ptr @${thunk}, i1 ${always.name})`, + ); + return host.own({ name: t, type: e.type }); + } return host.emitGenericLibCall(e); } +/** The entry of a callback bound to an AsyncLocalStorage context, one per + * callback signature: enters the captured context, calls the target with + * the same arguments (the callValue ABI) and restores the caller's + * context, also when the target leaves an exception pending. */ +function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string { + const key = `alsbound:${typeKey(cbT)}`; + let sym = host.resolveThunks.get(key); + if (sym) return sym; + sym = `sc_alsbound_${host.resolveThunks.size}`; + host.resolveThunks.set(key, sym); + host.declare("declare ptr @scr_als_bound_target(ptr)"); + host.declare("declare ptr @scr_als_bound_enter(ptr)"); + host.declare("declare void @scr_als_bound_leave(ptr, ptr)"); + const ret = host.llType(cbT.ret); + const params = cbT.params.map((p, i) => `${host.llType(p)} %a${i}`); + const call = `call ${ret} %fn(${["ptr %cb", ...params].join(", ")})`; + host.resolveThunkDefs.push( + `define internal ${ret} @${sym}(${["ptr %self", ...params].join(", ")}) ${FN_ATTRS} { ; AsyncLocalStorage-bound callback ${typeKey(cbT)}`, + `entry:`, + ` %cb = call ptr @scr_als_bound_target(ptr %self)`, + ` %prev = call ptr @scr_als_bound_enter(ptr %self)`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ret === "void" ? ` ${call}` : ` %r = ${call}`, + ` call void @scr_als_bound_leave(ptr %prev, ptr %cb)`, + ret === "void" ? ` ret void` : ` ret ${ret} %r`, + `}`, + ``, + ); + return sym; +} + export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "process.forkTarget") { diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 53c715108..5ed131567 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1438,6 +1438,10 @@ export const LIB_FN_SYMS: Record = { "als.exitRun": "scr_als_exit_run", "als.enterWith": "scr_als_enter_with", "als.disable": "scr_als_disable", + "als.newOptions": "scr_als_new_options", + "als.name": "scr_als_name", + "als.bindCallbackDyn": "scr_als_bind_dyn", + "als.snapshot": "scr_als_snapshot", "dc.chanBindStore": "scr_dc_chan_bind_store", "dc.chanUnbindStore": "scr_dc_chan_unbind_store", "dc.chanRunStores": "scr_dc_chan_run_stores", diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 18bd93a86..a130df59b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -2,7 +2,7 @@ import { isBuiltinClassInstance } from "./receiver-types.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, DYN, @@ -184,16 +184,89 @@ export function lowerDiagnosticsChannelMethodCall( ); } +/** Whether `node` names the AsyncLocalStorage class itself. */ +function isAsyncLocalStorageClass(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isIdentifier(node)) return false; + const symbol = lowerer.resolveValueSymbol(node); + return ( + symbol !== null && + lowerer.isStdlibSymbol(symbol) && + ts.symbolName(symbol) === "AsyncLocalStorage" + ); +} + +/** The static AsyncLocalStorage members: bind(fn) answers fn bound to the + * active context (its own type for a typed function; a dyn function in + * JavaScript files, where the bound function's name and receiver are + * observable, and for dyn values); snapshot() answers a dyn function + * (cb, ...args) that runs cb in the context active at the snapshot. */ +function lowerAsyncLocalStorageStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const name = access.name.text; + const loc = locOf(call); + const always: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + if (name === "bind" && call.arguments.nodes.length === 1) { + const arg = call.arguments.nodes[0]!; + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) { + const fn = lowerer.lowerExpr(arg); + if (fn.type.kind === "func") + return { kind: "libCall", fn: "als.bindCallback", args: [fn, always], type: fn.type, loc }; + } + return asyncLocalStorageResult(lowerer, call, { + kind: "libCall", + fn: "als.bindCallbackDyn", + args: [lowerDiagnosticsMessage(lowerer, arg), always], + type: DYN, + loc, + }); + } + if (name === "snapshot" && call.arguments.nodes.length === 0) + return { kind: "libCall", fn: "als.snapshot", args: [], type: DYN, loc }; + lowerer.noLowering( + `AsyncLocalStorage.${name}`, + call, + "AsyncLocalStorage.bind(fn) and AsyncLocalStorage.snapshot() are the supported static members", + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** new AsyncLocalStorage(options): the options object as a dyn value + * (plain property literals build it member by member). */ +export function lowerAsyncLocalStorageOptions(lowerer: Lowerer, node: ts.Expression): IrExpr { + return lowerTracingHandlers(lowerer, node); +} + +/** `.name` on AsyncLocalStorage receivers: the constructor's name option + * (als.name), converted to the property's type. Null otherwise. */ +export function lowerAsyncLocalStorageProperty( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || access.name.text !== "name") return null; + if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null; + if (!lowerer.isStdlibMember(access)) return null; + const loc = locOf(access); + const receiver = lowerer.lowerExprExpecting(access.expression, F64); + const value: IrExpr = { kind: "libCall", fn: "als.name", args: [receiver], type: DYN, loc }; + const expected = lowerer.mapTypeOf(lowerer.typeOf(access)); + return !expected || expected.kind === "dyn" ? value : lowerer.coerceInto(access, value, expected); +} + /** Method calls on AsyncLocalStorage receivers: run(store, fn, ...args), * exit(fn, ...args), getStore(), enterWith(store), disable() — over the - * f64 store handle (als.* libCalls; values cross as dyn values). Null - * for non-ALS receivers. */ + * f64 store handle (als.* libCalls; values cross as dyn values) — and the + * static bind/snapshot members. Null for other receivers. */ export function lowerAsyncLocalStorageMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; + if (isAsyncLocalStorageClass(lowerer, access.expression)) + return lowerAsyncLocalStorageStaticCall(lowerer, call, access); if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts new file mode 100644 index 000000000..402741da6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -0,0 +1,130 @@ +/* Completion callbacks in programs that use AsyncLocalStorage. Node runs + * the callback of an operation a callback-style API starts (fs.readFile, + * crypto.randomBytes, zlib.gzip, dns.lookup, server.listen, net.connect, + * http.get, ...) with the stores active when the call started it. The runtime invokes these callbacks from + * the event loop, where the context is empty, so in programs whose + * scheduling carries contexts (RuntimeFeatures asyncContext) each such + * argument is bound to the active context at the call: als.bindCallback + * for typed closures, als.bindCallbackDyn for dyn functions. Other + * programs are left unchanged. */ +import { BOOL, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import type { IrLibFn } from "../../ir/builtin-signatures.js"; +import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; +import { moduleRuntimeFeatures } from "../../ir/runtime-features.js"; + +/** The argument that is the completion callback: its index; "last" for + * calls whose callback is the final argument when one is passed; or + * "arguments" for the fs callback calls, whose argument is the call's + * dyn argument vector, where the runtime finds the callback. */ +type CallbackPosition = number | "last" | "arguments"; + +const COMPLETION_CALLBACKS: ReadonlyMap = new Map< + IrLibFn, + CallbackPosition +>([ + ["cp.execFile", 2], + ["crypto.generateKeyCb", 2], + ["crypto.hkdfCb", 5], + ["crypto.pbkdf2Cb", 5], + ["crypto.randomBytesCb", 1], + ["crypto.randomFillCb", 4], + ["crypto.randomFillCbDyn", 4], + ["crypto.randomIntCb", 3], + ["crypto.scryptCb", 4], + ["dgram.bindCb", 3], + ["dgram.closeCb", 1], + ["dgram.connectCb", 3], + ["dns.lookupDyn", 2], + ["fs.callbackCall", "arguments"], + ["fs.callbackGlob", "arguments"], + ["fs.renameCb", 2], + ["http.requestAgentCb", "last"], + ["http.requestCb", "last"], + ["http.requestConnCb", "last"], + ["http.requestDynCb", "last"], + ["http.requestUrlCb", "last"], + ["net.connectCb", 2], + ["net.listenCb", 2], + ["net.listenOptsCb", 4], + ["net.listenOptsReusePortCb", 5], + ["net.listenPathCb", 2], + ["process.stderrWriteBytesCb", 2], + ["process.stdoutWriteBytesCb", 2], + ["zlib.cbOpts", 3], + ["zlib.cbOptsDyn", 3], + ["zlib.deflateCb", 1], + ["zlib.deflateRawCb", 1], + ["zlib.gunzipCb", 1], + ["zlib.gzipCb", 1], + ["zlib.inflateCb", 1], + ["zlib.inflateRawCb", 1], + ["zlib.unzipCb", 1], +]); + +/** `cb` bound to the context active where it is evaluated. An empty + * context leaves it unbound (`always` false): the loop already runs + * callbacks with an empty context. */ +function bindToActiveContext(cb: IrExpr): IrExpr { + const always: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc: cb.loc }; + if (cb.type.kind === "func") + return { + kind: "libCall", + fn: "als.bindCallback", + args: [cb, always], + type: cb.type, + loc: cb.loc, + }; + if (cb.type.kind === "dyn") + return { + kind: "libCall", + fn: "als.bindCallbackDyn", + args: [cb, always], + type: cb.type, + loc: cb.loc, + }; + return cb; +} + +/** An element of a dyn argument vector that can be the callback: a boxed + * closure binds as a typed closure; any other dyn value that is not a + * boxed primitive binds as a dyn function (non-functions pass through). */ +function bindArgumentElement(e: IrExpr): IrExpr { + if (e.kind === "dynFrom") { + return e.value.type.kind === "func" ? { ...e, value: bindToActiveContext(e.value) } : e; + } + return e.type.kind === "dyn" ? bindToActiveContext(e) : e; +} + +function bindCall(e: IrExpr): IrExpr { + if (e.kind !== "libCall") return e; + const position = COMPLETION_CALLBACKS.get(e.fn); + if (position === undefined) return e; + if (position === "arguments") { + const index = e.fn === "fs.callbackCall" ? 1 : 0; + const vector = e.args[index]; + if (vector === undefined || vector.kind !== "dynArrLit") return e; + const args = e.args.slice(); + args[index] = { + kind: "dynArrLit", + elems: vector.elems.map(bindArgumentElement), + type: vector.type, + loc: vector.loc, + }; + return { ...e, args }; + } + const index = position === "last" ? e.args.length - 1 : position; + const cb = e.args[index]; + if (cb === undefined || (position === "last" && cb.type.kind !== "func")) return e; + const args = e.args.slice(); + args[index] = bindToActiveContext(cb); + return { ...e, args }; +} + +/** Binds the completion callbacks of `mod` when its scheduling carries + * AsyncLocalStorage contexts. */ +export function bindCompletionCallbacks(mod: IrModule): void { + if (!moduleRuntimeFeatures(mod).asyncContext) return; + const rewriteExpr = (e: IrExpr): IrExpr => bindCall(mapExprChildren(e, rewriteExpr, rewriteStmt)); + const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt); + for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7f3910c15..0777e8fb9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -134,6 +134,7 @@ import { import { expandoMemberRead } from "../lower-expando.js"; import { lowerProtocolPropertyRead } from "../class-protocol-dispatch.js"; import { lowerReadlineProperty } from "../builtins/readline.js"; +import { lowerAsyncLocalStorageProperty } from "../builtins/async-context.js"; import { lowerEventStaticProperty } from "../builtins/numbers.js"; import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js"; import { lowerValueStreamProperty } from "../builtins/hash-stream.js"; @@ -814,6 +815,8 @@ export function lowerPropertyAccessExpr( // diagnostics_channel Channel receivers — name/hasSubscribers // over the f64 channel handle. lowerer.lowerDiagnosticsChannelProperty(expr) ?? + // AsyncLocalStorage receivers — the name option. + lowerAsyncLocalStorageProperty(lowerer, expr) ?? // readline Interface receivers — the history and option members // over the f64 interface handle. lowerReadlineProperty(lowerer, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..14371f46b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,4 @@ +import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js"; import { DATE_TIME_FIELD, dateRootOf, @@ -10471,21 +10472,23 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { lowerer.badType(expr, tsType); } // `new AsyncLocalStorage()` (node:async_hooks): a fresh store id — - // an f64 handle (type-mapper.ts), the Channel story. Construction options - // ({ defaultValue, name }) have no lowering yet. + // an f64 handle (type-mapper.ts), the Channel story. The options form + // ({ defaultValue, name }) passes the options object as a dyn value. if ( symbol !== null && ts.symbolName(symbol) === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol) ) { - if ((expr.arguments?.nodes.length ?? 0) > 0) { - lowerer.noLowering( - "new AsyncLocalStorage(options)", - expr, - "the zero-argument constructor is the supported form (defaultValue/name options have no lowering yet)", - ); - } - return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; + const options = expr.arguments?.nodes[0]; + if (options === undefined) + return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; + return { + kind: "libCall", + fn: "als.newOptions", + args: [lowerAsyncLocalStorageOptions(lowerer, options)], + type: F64, + loc, + }; } // `new Promise((resolve) => ...)`: the ambient Promise constructor. if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index edf0a694b..fd2f44d73 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -29,6 +29,7 @@ import { refuseUncheckedCycleReads, type CycleInitFlag, } from "./cycle-initialization.js"; +import { bindCompletionCallbacks } from "./completion-callbacks.js"; import { isJsonRequireDecl, type JsonRequireModule } from "./json-require.js"; import { checkRequireBindingAccesses, @@ -7678,6 +7679,7 @@ export class Lowerer { }; if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name)); if (module) checkCycleBindingAccesses(this, module); + if (module) bindCompletionCallbacks(module); // Execution tiers from the final bodies (ir/tiers.ts). Assigned with or // without a module: coverage reports the dynamic tier of programs that // still have blockers. diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index e6c5c4c93..535c2b584 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4674,6 +4674,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (hybrid) return hybrid; } const callSigs = checker.getCallSignatures(widened); + // AsyncLocalStorage.snapshot()'s generic runner is a dyn function value + // (als.snapshot), called through the dyn call path. + if (callSigs.length === 1 && isAsyncLocalStorageSnapshot(callSigs[0]!, ctx)) return DYN; if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx); // Records: object types whose members are all data properties (shorthand // methods in type position count — they're func-typed fields) with @@ -5004,6 +5007,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { /** One non-overloaded call signature's func type (mapTypeInner's * single-signature rule; erasedOverloadMemberType maps each overload of a * member through it). */ +/** Whether `sig` is the function type AsyncLocalStorage.snapshot() returns: + * declared as that static method's return type in the async_hooks module + * of the standard library declarations. */ +function isAsyncLocalStorageSnapshot(sig: ts.Signature, ctx: TypeMapperCtx): boolean { + const decl = sig.declaration; + if (decl === undefined || !ts.isFunctionTypeNode(decl)) return false; + const method = decl.parent; + return ( + method !== undefined && + ts.isMethodDeclaration(method) && + ts.isIdentifier(method.name) && + method.name.text === "snapshot" && + method.parent !== undefined && + ts.isClassDeclaration(method.parent) && + method.parent.name?.text === "AsyncLocalStorage" && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && + isDeclaredInAmbientModule(decl, "async_hooks") + ); +} + function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null { const { checker } = ctx; if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index c202b8538..34a0a2b14 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -194,6 +194,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "async.awaitDyn", "als.run", "als.exitRun", + "als.newOptions", + "als.bindCallbackDyn", "dc.chanBindStore", "dc.chanRunStores", "http.resWriteHeadDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f37e8809f..f426cb6c7 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4592,6 +4592,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN }, "als.enterWith": { argTypes: [F64, DYN], result: VOID }, "als.disable": { argTypes: [F64], result: VOID }, + /** new AsyncLocalStorage(options) (Node's object check throws; MAY + * THROW) and the store's name option (dyn; "" without one). */ + "als.newOptions": { argTypes: [DYN], result: F64 }, + "als.name": { argTypes: [F64], result: DYN }, + /** A function bound to the active context: the completion callbacks of + * callback-style APIs (always false: an empty context leaves the + * callback unbound) and AsyncLocalStorage.bind (always true). The typed + * form answers its closure argument's type; the dyn form throws Node's + * ERR_INVALID_ARG_TYPE for a non-function when always (MAY THROW). */ + "als.bindCallback": { argTypes: [null, BOOL], result: VOID }, + "als.bindCallbackDyn": { argTypes: [DYN, BOOL], result: DYN }, + /** AsyncLocalStorage.snapshot(): a dyn function (cb, ...args) that runs + * cb in the context active at the snapshot. */ + "als.snapshot": { argTypes: [], result: DYN }, /** Channel.bindStore/unbindStore/runStores (scr_dc.c): the * AsyncLocalStorage integration — set-semantics bindings per store, * runStores entering every bound store with transform(data) around the diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5c30b0f69..05ebe2496 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -115,12 +115,16 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "perf.measure", // Blob and File (scr_blob.c, linked with this unit, which boxes their // reads as dyn promises). + "als.bindCallbackDyn", "als.disable", "als.enterWith", "als.exitRun", "als.get", + "als.name", "als.new", + "als.newOptions", "als.run", + "als.snapshot", "blob.arrayBuffer", "blob.bytes", "blob.fileNew", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..915f7a4af 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5310,6 +5310,13 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { + if (e.fn === "als.bindCallback") { + // The bound function, at its closure argument's own type. + const fn = e.args[0]?.type; + if (fn?.kind !== "func" || !typeEquals(e.type, fn)) + err(`libCall ${e.fn} must return its function argument's type`, e.loc); + return true; + } if ( e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest" || diff --git a/packages/compiler/test/completion-callbacks.test.ts b/packages/compiler/test/completion-callbacks.test.ts new file mode 100644 index 000000000..817472c19 --- /dev/null +++ b/packages/compiler/test/completion-callbacks.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-completion-callbacks-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const randomBytes = ` +import { randomBytes } from "node:crypto"; +randomBytes(4, (error, buffer) => console.log(error === null, buffer.length)); +`; + +test("programs without AsyncLocalStorage pass completion callbacks unchanged", async () => { + const llvm = await emit(randomBytes); + expect(llvm).not.toContain("@scr_als_bind_closure"); + expect(llvm).not.toContain("sc_alsbound_"); +}); + +test("programs that enter stores bind each completion callback to the active context", async () => { + const llvm = await emit(` +import { AsyncLocalStorage } from "node:async_hooks"; +${randomBytes} +const store = new AsyncLocalStorage(); +store.run("a", () => randomBytes(1, (error, buffer) => console.log(store.getStore(), buffer.length))); +`); + const binds = llvm.match( + /call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 false\)/g, + ); + expect(binds?.length).toBe(2); + // One thunk per callback signature. + expect(llvm.match(/^define internal void @sc_alsbound_\d+\(/gm)?.length).toBe(1); +}); + +test("AsyncLocalStorage.bind binds a typed function with an empty context too", async () => { + const llvm = await emit(` +import { AsyncLocalStorage } from "node:async_hooks"; +const store = new AsyncLocalStorage(); +const read = AsyncLocalStorage.bind((n: number) => \`\${store.getStore()} \${n}\`); +console.log(store.run("x", () => read(1))); +`); + expect(llvm).toMatch( + /call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 true\)/, + ); + expect(llvm).toMatch(/^define internal ptr @sc_alsbound_\d+\(ptr %self, double %a0\)/m); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json new file mode 100644 index 000000000..5645ac5df --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7110-als-completion-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json new file mode 100644 index 000000000..27aa14a8d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7111-als-bind-snapshot-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json new file mode 100644 index 000000000..1915cae63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..992ec3767 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4012,6 +4012,47 @@ double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_ void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } +/* A typed callback bound to the context active when it was bound: the + * completion callbacks of callback-style APIs in programs that use + * AsyncLocalStorage, and AsyncLocalStorage.bind. The bound closure's entry + * is a thunk the compiler emits per callback signature; it reads the + * target (scr_als_bound_target), enters the captured context + * (scr_als_bound_enter), calls the target with its own arguments and + * restores the caller's context (scr_als_bound_leave). With `always` + * false an empty context leaves the callback unbound, since the loop runs + * callbacks with an empty context; AsyncLocalStorage.bind passes true, so + * a bound function called later inside run() still sees no store. */ +ScrClosure *scr_als_bind_closure(ScrClosure *cb /*moves*/, void *thunk, bool always) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (cb == NULL || (active == NULL && !always)) return cb; + ScrClosure *bound = scr_closure_new(thunk, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + return bound; +} + +/* The bound target (+1, released by scr_als_bound_leave). */ +ScrClosure *scr_als_bound_target(ScrClosure *self) { + return scr_box_get_ref(self->caps[0]); +} + +/* Enters the captured context; returns the caller's (owned). */ +ScrAlsCtx *scr_als_bound_enter(ScrClosure *self) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[1]); /* the slot owns this reference */ + return prev; +} + +/* Restores the caller's context, replacing whatever the target entered + * (an enterWith inside it), and releases the target. */ +void scr_als_bound_leave(ScrAlsCtx *prev, ScrClosure *target) { + scr_als_restore(prev); + scr_closure_release(target); +} + /* The checked-dynamic argument form (JS files — common.mustCall wrappers * and the suite's invalid-input probes): a non-function throws Node's * ERR_INVALID_ARG_TYPE synchronously; a function value is called with diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..003bf3be8 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -198,6 +198,14 @@ void scr_als_disable(double id); * argument vector, restore (the finally). Result +1 or NULL pending. */ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args); ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args); +/* new AsyncLocalStorage(options) (options borrowed; NaN with the exception + * pending for a non-object) and the name it carries (+1). */ +double scr_als_new_options(const ScrDyn *options); +ScrDyn *scr_als_name(double id); +/* AsyncLocalStorage.bind over a dyn function and the dyn completion + * callbacks (fn borrowed; +1), and AsyncLocalStorage.snapshot() (+1). */ +ScrDyn *scr_als_bind_dyn(ScrDyn *fn, bool always); +ScrDyn *scr_als_snapshot(void); /* process.on/once('unhandledRejection', fn): dyn listeners dispatched * per never-observed rejection at the end of its nextTick/microtask @@ -473,6 +481,13 @@ double scr_set_timeout_handle_als(ScrClosure *cb, double ms); double scr_set_immediate_als(ScrClosure *cb); void scr_next_tick_als(ScrClosure *cb); void scr_queue_microtask_als(ScrClosure *cb); +/* A typed callback bound to the active context (scr_async.c): `thunk` is + * the compiler's per-signature entry, which brackets the call with + * scr_als_bound_target/enter/leave. cb moves in; the result is +1. */ +ScrClosure *scr_als_bind_closure(ScrClosure *cb, void *thunk, bool always); +ScrClosure *scr_als_bound_target(ScrClosure *self); +ScrAlsCtx *scr_als_bound_enter(ScrClosure *self); +void scr_als_bound_leave(ScrAlsCtx *prev, ScrClosure *target); /* A successful process.stdout/stderr.write completion on that same queue. * The callback and its program-shaped Error | null adapter both MOVE into * the entry; the adapter is invoked with NULL (Node's success argument). */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a6cd813de..c47626557 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -11,6 +11,7 @@ * 'warning' listeners + Node's stderr report). */ #include "scr_runtime.h" +#include #include #include #include @@ -30,6 +31,21 @@ static void scr_ad_oom(void) { static SCR_TL double scr_als_counter = 0; +/* The constructor options of each store, indexed by id - 1: the default + * value getStore() answers where the store has no entry, and the name. + * NULL until a store with options exists. */ +typedef struct { + ScrDyn *default_value; /* NULL: no default */ + ScrDyn *name; /* NULL: "" */ +} ScrAlsOptions; +static SCR_TL ScrAlsOptions *scr_als_options = NULL; +static SCR_TL size_t scr_als_options_len = 0; + +static const ScrAlsOptions *scr_als_options_of(double id) { + size_t i = (size_t)id - 1; + return id >= 1 && i < scr_als_options_len ? &scr_als_options[i] : NULL; +} + /* Main-slot teardown (atexit, LIFO after scr_init's audit registration — * the dc-registry precedent): a top-level enterWith leaves a context in * the main slot at exit; release it before the RC audit counts. */ @@ -37,6 +53,13 @@ static void scr_als_teardown(void) { /* atexit runs on the main context — the active slot IS main's. */ scr_als_ctx_release(*SCR_ALS_SLOT()); *SCR_ALS_SLOT() = NULL; + for (size_t i = 0; i < scr_als_options_len; i++) { + scr_dyn_release(scr_als_options[i].default_value); + scr_dyn_release(scr_als_options[i].name); + } + free(scr_als_options); + scr_als_options = NULL; + scr_als_options_len = 0; } double scr_als_new(void) { @@ -48,6 +71,48 @@ double scr_als_new(void) { return ++scr_als_counter; } +/* new AsyncLocalStorage(options): Node validates that options is an + * object, then keeps options.defaultValue and options.name. NaN with the + * exception pending when options is not an object. */ +double scr_als_new_options(const ScrDyn *options) { + if (options->kind == SCR_DYN_UNDEF) return scr_als_new(); + /* A typed record or class instance reads through its snapshot. */ + ScrDyn *snapshot = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL; + if (snapshot) options = snapshot; + if (options->kind != SCR_DYN_OBJ) { + scr_dyn_arg_type_fail("options", "of type object", options); + scr_dyn_release(snapshot); + return NAN; + } + double id = scr_als_new(); + size_t index = (size_t)id - 1; + if (index >= scr_als_options_len) { + size_t len = index + 1; + ScrAlsOptions *grown = realloc(scr_als_options, len * sizeof *grown); + if (!grown) scr_ad_oom(); + for (size_t i = scr_als_options_len; i < len; i++) grown[i] = (ScrAlsOptions){NULL, NULL}; + scr_als_options = grown; + scr_als_options_len = len; + } + ScrDyn *value = scr_dyn_obj_get(options, "defaultValue", 12); + ScrDyn *name = scr_dyn_obj_get(options, "name", 4); + scr_als_options[index].default_value = scr_dyn_retain(value ? value : scr_dyn_undefined()); + scr_als_options[index].name = + name && name->kind != SCR_DYN_UNDEF ? scr_dyn_retain(name) : NULL; + scr_dyn_release(snapshot); + return id; +} + +/* asyncLocalStorage.name (+1): the constructor's name option, or "". */ +ScrDyn *scr_als_name(double id) { + const ScrAlsOptions *options = scr_als_options_of(id); + if (options && options->name) return scr_dyn_retain(options->name); + ScrStr *empty = scr_str_new("", 0); + ScrDyn *result = scr_dyn_new_str(empty); + scr_str_release(empty); + return result; +} + ScrDyn *scr_als_get(double id) { const ScrAlsCtx *c = *SCR_ALS_SLOT(); if (c) { @@ -55,6 +120,8 @@ ScrDyn *scr_als_get(double id) { if (c->entries[i].id == id) return scr_dyn_retain(c->entries[i].value); } } + const ScrAlsOptions *options = scr_als_options_of(id); + if (options && options->default_value) return scr_dyn_retain(options->default_value); return scr_dyn_retain(scr_dyn_undefined()); } @@ -104,8 +171,77 @@ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args) { return scr_als_call_in(scr_als_enter(id, value), fn, args); } +/* exit() runs fn with the store undefined: a store with a default value + * holds an undefined entry for the call (getStore() answers undefined, + * not the default, as in Node); other stores drop their entry. */ ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args) { - return scr_als_call_in(scr_als_enter_absent(id), fn, args); + const ScrAlsOptions *options = scr_als_options_of(id); + ScrAlsCtx *prev = options && options->default_value + ? scr_als_enter(id, scr_dyn_undefined()) + : scr_als_enter_absent(id); + return scr_als_call_in(prev, fn, args); +} + +/* AsyncLocalStorage.bind(fn) and the dyn completion callbacks of + * callback-style APIs: a function that calls fn in the context active + * when it was bound, with its own arguments and receiver. caps[0] holds + * fn, caps[1] the context, as scr_als_bind_closure lays them out. */ +static void *scr_als_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_als_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static ScrDyn *scr_als_bound_dyn_call(ScrClosure *self, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *prev = scr_als_bound_enter(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, "callback"); + scr_als_restore(prev); + scr_dyn_release(fn); + return result; +} + +static ScrClosure *scr_als_context_closure(void *fn, size_t ncaps) { + ScrClosure *closure = scr_closure_new(fn, ncaps); + closure->caps[ncaps - 1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(closure->caps[ncaps - 1], scr_als_ctx_retain(*SCR_ALS_SLOT())); + return closure; +} + +/* fn borrowed; +1. With `always` false an empty context answers fn + * itself (the completion-callback form). AsyncLocalStorage.bind throws + * Node's ERR_INVALID_ARG_TYPE for a non-function. */ +ScrDyn *scr_als_bind_dyn(ScrDyn *fn, bool always) { + if (fn->kind != SCR_DYN_FUNC) { + if (always) { + scr_dyn_arg_type_fail("fn", "of type function", fn); + return NULL; + } + return scr_dyn_retain(fn); + } + if (!always && *SCR_ALS_SLOT() == NULL) return scr_dyn_retain(fn); + ScrClosure *closure = scr_als_context_closure(NULL, 2); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_retain(fn)); + return scr_dyn_new_func(closure, scr_als_bound_dyn_call, fn->v.fn.arity, "(...args)", "bound"); +} + +/* AsyncLocalStorage.snapshot(): a function (cb, ...args) that calls cb + * with args in the context active at the snapshot. */ +static ScrDyn *scr_als_snapshot_call(ScrClosure *self, ScrDyn *const *args, size_t argc) { + const ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (cb->kind != SCR_DYN_FUNC) { + static const char message[] = "cb is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[0]); /* the slot owns this reference */ + ScrDyn *result = scr_dyn_call(cb, args + 1, argc - 1, "callback"); + scr_als_restore(prev); + return result; +} + +ScrDyn *scr_als_snapshot(void) { + return scr_dyn_new_func(scr_als_context_closure(NULL, 1), scr_als_snapshot_call, 1, "(cb, ...args)", "bound"); } /* The two rejection-event registries share one shape: listeners with a diff --git a/tests/corpus/7110-als-completion-callbacks.ts b/tests/corpus/7110-als-completion-callbacks.ts new file mode 100644 index 000000000..4b4e97e0d --- /dev/null +++ b/tests/corpus/7110-als-completion-callbacks.ts @@ -0,0 +1,135 @@ +// The completion callback of a callback-style API runs with the +// AsyncLocalStorage stores that were active when the call started the +// operation, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; +import { execFile } from "node:child_process"; +import * as crypto from "node:crypto"; +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import { readFile, rename, writeFileSync, unlinkSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); + +// Starts one operation inside run(name) and waits for its callback. +function step(name: string, start: (done: () => void) => void): Promise { + return new Promise((resolve) => { + store.run(name, () => + start(() => { + console.log(name, store.getStore()); + resolve(); + }), + ); + }); +} + +const dir = tmpdir(); +const file = join(dir, `scriptc-als-${process.pid}.txt`); +writeFileSync(file, "hello"); + +await step("fs.readFile", (done) => readFile(file, "utf8", () => done())); +await step("fs.rename", (done) => rename(file, `${file}.moved`, () => done())); +unlinkSync(`${file}.moved`); + +await step("crypto.randomBytes", (done) => crypto.randomBytes(4, () => done())); +await step("crypto.randomInt", (done) => crypto.randomInt(10, () => done())); +await step("crypto.randomFill", (done) => crypto.randomFill(new Uint8Array(4), () => done())); +await step("crypto.pbkdf2", (done) => crypto.pbkdf2("p", "s", 1, 8, "sha256", () => done())); +await step("crypto.scrypt", (done) => crypto.scrypt("p", "s", 8, () => done())); +await step("crypto.hkdf", (done) => crypto.hkdf("sha256", "k", "s", "i", 8, () => done())); +await step("crypto.generateKey", (done) => + crypto.generateKey("hmac", { length: 64 }, () => done()), +); + +await step("zlib.gzip", (done) => zlib.gzip(Buffer.from("hello"), () => done())); +await step("zlib.deflate options", (done) => + zlib.deflate(Buffer.from("hello"), { level: 1 }, () => done()), +); + +await step("dns.lookup", (done) => dns.lookup("localhost", () => done())); +await step("child_process.execFile", (done) => execFile("true", [], () => done())); + +// A server's listen callback, a client connection and an HTTP response +// each run in the stores of their own call. +const server = net.createServer((socket) => socket.end()); +await step("net listen", (done) => server.listen(0, "127.0.0.1", () => done())); +const port = (server.address() as net.AddressInfo).port; +await step("net.connect", (done) => { + const socket = net.connect(port, "127.0.0.1", () => { + socket.destroy(); + done(); + }); +}); +server.close(); + +const web = http.createServer((_req, res) => res.end("ok")); +await new Promise((resolve) => web.listen(0, "127.0.0.1", () => resolve())); +const webPort = (web.address() as net.AddressInfo).port; +await step("http.get", (done) => + http.get(`http://127.0.0.1:${webPort}/`, (res) => { + res.resume(); + done(); + }), +); +await step("http.request", (done) => + http + .request({ host: "127.0.0.1", port: webPort, path: "/", method: "GET" }, (res) => { + res.resume(); + done(); + }) + .end(), +); +web.close(); + +const optionsServer = net.createServer(); +await step("net listen options", (done) => + optionsServer.listen({ port: 0, host: "127.0.0.1" }, () => done()), +); +optionsServer.close(); +const pathServer = net.createServer(); +const socketPath = join(dir, `scriptc-als-${process.pid}.sock`); +await step("net listen path", (done) => pathServer.listen(socketPath, () => done())); +pathServer.close(); + +const udp = dgram.createSocket("udp4"); +await step("dgram bind", (done) => udp.bind(0, "127.0.0.1", () => done())); +const peer = dgram.createSocket("udp4"); +await new Promise((resolve) => peer.bind(0, "127.0.0.1", () => resolve())); +await step("dgram connect", (done) => udp.connect(peer.address().port, "127.0.0.1", () => done())); +peer.close(); +await step("dgram close", (done) => udp.close(() => done())); + +await step("process.stdout.write", (done) => process.stdout.write("", () => done())); + +// Nested: a callback starts another operation in a different store. +await step("outer", (done) => + crypto.randomBytes(1, () => { + console.log("outer callback", store.getStore()); + store.run("inner", () => + crypto.randomBytes(1, () => { + console.log("inner callback", store.getStore()); + done(); + }), + ); + }), +); + +// A callback whose call ran with no store sees no store, and a store the +// callback enters does not reach the next callback. +await new Promise((resolve) => + crypto.randomBytes(1, () => { + console.log("no store", store.getStore()); + store.enterWith("entered"); + resolve(); + }), +); +await new Promise((resolve) => + crypto.randomBytes(1, () => { + console.log("after enterWith", store.getStore()); + resolve(); + }), +); diff --git a/tests/corpus/7111-als-bind-snapshot-options.ts b/tests/corpus/7111-als-bind-snapshot-options.ts new file mode 100644 index 000000000..d1c6dbbf2 --- /dev/null +++ b/tests/corpus/7111-als-bind-snapshot-options.ts @@ -0,0 +1,59 @@ +// AsyncLocalStorage.bind, AsyncLocalStorage.snapshot and the constructor +// options, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); + +// bind(fn) runs fn in the context active at the bind, with its arguments. +const add = store.run("bound", () => + AsyncLocalStorage.bind((a: number, b: number) => `${store.getStore()}:${a + b}`), +); +console.log(add(1, 2)); +console.log(store.run("other", () => add(3, 4))); + +// A function bound outside any store sees no store inside run(). +const plain = AsyncLocalStorage.bind(() => store.getStore()); +console.log(store.run("ignored", () => plain())); + +// The bound function keeps its result type. +const length = store.run("len", () => AsyncLocalStorage.bind((s: string) => s.length)); +console.log(length("four") * 2); + +// snapshot() runs a function in the context of the snapshot. +const snap = store.run("snapshot", () => AsyncLocalStorage.snapshot()); +console.log(snap(() => store.getStore())); +console.log(store.run("later", () => snap(() => store.getStore()))); +console.log(snap((x: number, y: number) => x * y, 6, 7)); + +// A store entered inside the bound call ends with it. +const enter = store.run("start", () => + AsyncLocalStorage.bind(() => { + store.enterWith("entered"); + return store.getStore(); + }), +); +console.log(enter(), store.getStore()); + +// The default value and the name. +const config = new AsyncLocalStorage<{ level: number }>({ + defaultValue: { level: 1 }, + name: "config", +}); +console.log(config.name, config.getStore()?.level); +config.run({ level: 2 }, () => console.log("run", config.getStore()?.level)); +config.exit(() => console.log("exit", config.getStore())); +config.disable(); +console.log("disabled", config.getStore()?.level); +const unnamed = new AsyncLocalStorage(); +console.log(JSON.stringify(unnamed.name), unnamed.getStore()); +const empty = new AsyncLocalStorage({}); +console.log(JSON.stringify(empty.name), empty.getStore()); + +// Bound functions and snapshots carry the context across asynchronous work. +const later = store.run("timer", () => AsyncLocalStorage.bind(() => store.getStore())); +await new Promise((resolve) => + setTimeout(() => { + console.log("in timer", later(), store.getStore()); + resolve(); + }, 1), +); diff --git a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs new file mode 100644 index 000000000..22e3238b8 --- /dev/null +++ b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs @@ -0,0 +1,51 @@ +// AsyncLocalStorage.bind, snapshot and the constructor options over +// untyped values, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; +import { readFile } from "node:fs"; + +const store = new AsyncLocalStorage(); + +function sum(a, b) { + return [this?.tag, a + b, store.getStore()]; +} +const bound = store.run("bound", () => AsyncLocalStorage.bind(sum)); +console.log(bound.name, bound.length, bound === sum, typeof bound); +console.log(JSON.stringify(bound.call({ tag: "receiver" }, 1, 2))); +console.log(JSON.stringify(store.run("other", () => bound(3, 4)))); + +const snap = store.run("snapshot", () => AsyncLocalStorage.snapshot()); +console.log(snap.name, snap.length, typeof snap); +console.log(snap((a, b) => `${store.getStore()} ${a + b}`, 2, 3)); + +for (const attempt of [ + () => AsyncLocalStorage.bind(5), + () => AsyncLocalStorage.bind("fn"), + () => snap(5), + () => snap(), +]) { + try { + console.log("ok", attempt()); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +for (const options of [1, "x", null, true]) { + try { + new AsyncLocalStorage(options); + console.log("ok", options); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +const named = new AsyncLocalStorage({ name: "named", defaultValue: 0 }); +console.log(named.name, named.getStore()); +named.run(5, () => console.log(named.getStore())); +console.log(new AsyncLocalStorage(undefined).name === ""); + +// An untyped callback runs in the stores of its call. +store.run("readFile", () => + readFile(import.meta.filename, "utf8", (error, text) => + console.log("readFile", store.getStore(), error === null, text.length > 0), + ), +); diff --git a/tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json b/tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json new file mode 100644 index 000000000..d8f3f6d12 --- /dev/null +++ b/tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json @@ -0,0 +1 @@ +{ "plain": 6500, "san": 16000 } diff --git a/tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json b/tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } diff --git a/tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json b/tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json new file mode 100644 index 000000000..3fd934f0e --- /dev/null +++ b/tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 12000 } From ef8cc7f74f13470c21d3c47b92df75ad9055808a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:48:55 -0500 Subject: [PATCH 02/45] Settle Promise.all over different entry types on Node's turns `Promise.all([a, b])` whose entries have different types (the checker's tuple overload) ran through an async helper that awaited the combinator and then each fulfilled entry, so its awaits resumed three microtask turns after Node's. The combinator now stores the entry promises, and an adapter run inside its settlement reads them into the tuple, so the result settles on the combinator's turn: awaiting it, storing it first, chaining it with then and returning it from an async function all land on Node's turns. Plain entries join the combinator through Promise.resolve, as in Node, and unit-typed entries compile. --- .../compiler/src/backend/llvm/lib-dispatch.ts | 88 ++++++ .../frontend/lowering/builtins/promises.ts | 273 +++++++++--------- .../compiler/src/ir/builtin-signatures.ts | 12 + packages/compiler/src/ir/validate.ts | 26 ++ .../7035-promise-all-tuple-turns.ts.json | 6 + packages/runtime/src/scr_async.c | 6 + packages/runtime/src/scr_async.h | 2 + tests/corpus/7035-promise-all-tuple-turns.ts | 64 ++++ .../7035-promise-all-tuple-turns.ts.json | 1 + 9 files changed, 338 insertions(+), 140 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json create mode 100644 tests/corpus/7035-promise-all-tuple-turns.ts create mode 100644 tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 24f2bf019..207132d05 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -5,6 +5,7 @@ import { emitTlsLibCall } from "./lib-tls.js"; import { emitHttp2LibCall } from "./lib-http2.js"; import { emitDatagramLibCall } from "./lib-datagram.js"; import { emitTestLibCall } from "./lib-test.js"; +import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js"; /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; @@ -236,6 +237,32 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll host.emitPendingCheck(); return output; } + if (e.fn === "promise.allTuple") return emitPromiseAllTuple(host, e); + if (e.fn === "promise.fulfilledValue") { + // The payload of a fulfilled promise (borrowed operand): numbers and + // booleans by value, strings and references +1. + const promise = host.emitExpr(e.args[0]!); + const out = B.tmp(); + switch (e.type.kind) { + case "f64": + case "procStream": + host.declare(`declare double @scr_promise_payload_f64(ptr)`); + B.line(`${out} = call double @scr_promise_payload_f64(ptr ${promise.name})`); + return { name: out, type: e.type }; + case "bool": + host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`); + B.line(`${out} = call zeroext i1 @scr_promise_payload_bool(ptr ${promise.name})`); + return { name: out, type: e.type }; + case "string": + host.declare(`declare ptr @scr_promise_payload_str(ptr)`); + B.line(`${out} = call ptr @scr_promise_payload_str(ptr ${promise.name}) ; +1`); + return host.own({ name: out, type: e.type }); + default: + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + B.line(`${out} = call ptr @scr_promise_payload_ref(ptr ${promise.name}) ; +1`); + return host.own({ name: out, type: e.type }); + } + } if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") { const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null; if (e.fn === "dyn.typedRefKeyIs" && literal === null) @@ -877,6 +904,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "disposable": case "punycode": case "constants": + case "promise": return host.emitGenericLibCall(e); case "test": return emitTestLibCall(host, e); @@ -891,3 +919,63 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { } } } + +/** Promise.all's tuple form (promise.allTuple): the combinator over the + * entries stores each fulfilled entry promise, and a settlement adapter on + * the combinator calls the builder with them and fulfills the result with + * the tuple inside the combinator's settlement; a rejection settles the + * result the same way. The combinator itself counts as handled. */ +function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue { + const B = host.B; + const build = e.args[1]!; + if (build.kind !== "closure" || e.type.kind !== "promise") + throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape"); + const entriesT = e.args[0]!.type; + const entries = host.emitExpr(e.args[0]!); + const tuple = e.type.inner; + const key = `allTuple:${build.fnName}`; + let adapter = host.resolveThunks.get(key); + if (adapter === undefined) { + adapter = `sc_all_tuple_${host.resolveThunks.size}`; + host.resolveThunks.set(key, adapter); + const rc = vAdapters(host.shapeHost, tuple); + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + host.resolveThunkDefs.push( + `define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`, + `entry:`, + ` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`, + ` %tuple = call ptr @${host.callTarget(build.fnName)}(ptr %entries)`, + ` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`, + ` ret void`, + `}`, + ``, + ); + } + host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`); + host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`); + host.declare(`declare ptr @scr_promise_new()`); + host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`); + host.declare(`declare void @scr_promise_mark_handled(ptr)`); + host.declare(`declare void @scr_promise_release(ptr)`); + host.declare(`declare double @scr_arr_len(ptr)`); + if (entriesT.kind !== "array") + throw new InternalCompilerError("llvm emitter bug: promise.allTuple entries"); + const len = B.tmp(); + const cap = B.tmp(); + B.line(`${len} = call double @scr_arr_len(ptr ${entries.name})`); + B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`); + const values = B.tmp(); + B.line(`${values} = ${arrNewCall(host.shapeHost, entriesT.elem, cap)}`); + host.own({ name: values, type: entriesT }); + const all = B.tmp(); + B.line( + `${all} = call ptr @scr_promise_all(ptr ${entries.name}, ptr ${values}, ptr @scr_promise_all_store_promise)`, + ); + B.line(`call void @scr_promise_mark_handled(ptr ${all})`); + const result = B.tmp(); + B.line(`${result} = call ptr @scr_promise_new()`); + B.line(`call void @scr_promise_race_add(ptr ${result}, ptr ${all}, ptr @${adapter})`); + B.line(`call void @scr_promise_release(ptr ${all})`); + return host.own({ name: result, type: e.type }); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5..c30590619 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -2,7 +2,7 @@ import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; -import { newFnCtx, type Lowerer } from "../lowerer.js"; +import type { Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -15,32 +15,25 @@ import { type IrType, UNDEFINED_T, VOID, + F64, arrayOf, + funcOf, isUnitType, typeEquals, typeKey, } from "../../../ir/ir.js"; /** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's - * tuple overload, `Promise<[A, B, C]>`. One interned async helper per - * entry/result signature takes the entries as arguments, awaits the - * runtime combinator over the same promises (each widened to - * Promise, a type-only view), so settlement is Node's exactly: the - * first rejection in settlement order wins and every later one counts as - * handled; then it reads each (now fulfilled) entry and returns the - * tuple. Entries keep their identity — no value converts through the - * checked-dynamic tree. Non-promise entries (numbers, strings, booleans, - * unit values — never thenable) pass straight into their slot; a - * checked-dynamic entry (a JavaScript function's result) joins through - * PromiseResolve: the promise it holds, a promise that adopts a thenable, - * or a promise fulfilled with its plain value. The tuple - * fulfills one microtask turn after the combinator does. Null when the - * result type or an entry has no typed home here. */ -function lowerTuplePromiseAll( + * tuple overload, `Promise<[A, B, C]>`: the result type and tuple shape, + * or null when the result type or an entry has no typed home here. + * Entries are promises, checked-dynamic values (a JavaScript function's + * result) and plain numbers, strings, booleans and unit values, which are + * never thenable. */ +function tuplePromiseAllShape( lowerer: Lowerer, call: ts.CallExpression, - elems: IrExpr[], -): IrExpr | null { + elems: readonly IrExpr[], +): { resultT: IrType & { kind: "promise" }; tuple: IrType & { kind: "record" } } | null { const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.inner.shapeId); @@ -52,135 +45,135 @@ function lowerTuplePromiseAll( const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn"; if (!elems.some((e) => settles(e.type))) return null; if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null; + return { resultT, tuple: resultT.inner }; +} + +/** `Promise.all([p, q, r])` over different promise types. Each entry + * becomes a promise in input order (a plain value through Promise.resolve, + * a checked-dynamic one through its PromiseResolve view: the promise it + * holds, a promise that adopts a thenable, or a promise fulfilled with the + * value), widened to Promise, a type-only view. The runtime + * combinator over them gives Node's settlement: the first rejection in + * settlement order wins and every later one counts as handled. The + * result settles inside the combinator's own settlement, so it and its + * awaits land on Node's microtask turns; a fulfillment runs one interned + * function per entry/result signature that reads the fulfilled entries + * into the tuple. Entries keep their identity: no value converts through + * the checked-dynamic tree. */ +function lowerTuplePromiseAll( + lowerer: Lowerer, + call: ts.CallExpression, + elems: IrExpr[], +): IrExpr | null { + const plan = tuplePromiseAllShape(lowerer, call, elems); + if (!plan) return null; + const { resultT, tuple } = plan; const loc = locOf(call); - const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; + const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; + const entriesT = arrayOf(voidPromise); + const dynPromise: IrType = { kind: "promise", inner: DYN }; + const entries = elems.map((e): IrExpr => { + let promise: IrExpr; + if (e.type.kind === "promise") promise = e; + else if (e.type.kind === "dyn") + promise = { + kind: "libCall", + fn: "dyn.promiseResolve", + args: [e, thenReader(lowerer, loc)], + type: dynPromise, + loc, + }; + else if (isUnitType(e.type)) { + // A unit entry's value is its type's one value; the promise only + // takes its place in the combinator. + const resolved: IrExpr = { + kind: "intrinsic", + name: "promise.resolve", + args: [], + type: voidPromise, + loc, + }; + promise = + e.kind === "unitLit" + ? resolved + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: e, loc }], + result: resolved, + type: voidPromise, + loc, + }; + } else + promise = { + kind: "intrinsic", + name: "promise.resolve", + args: [e], + type: { kind: "promise", inner: e.type }, + loc, + }; + return promise.type.kind === "promise" && promise.type.inner.kind === "void" + ? promise + : { kind: "promiseVoidWiden", value: promise, type: voidPromise, loc }; + }); + const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(tuple)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); - const ctx = newFnCtx(false, null, null, resultT.inner); - ctx.isAsync = true; - lowerer.fnStack.push(ctx); - try { - const params = elems.map((e, i) => ({ - localId: `entry.${i}`, - name: `entry${i}`, - type: e.type, - })); - const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc); - const dynPromise: IrType = { kind: "promise", inner: DYN }; - const body: IrStmt[] = []; - // Each dyn entry's promise view, in entry order: the promise it - // holds, a promise that adopts a thenable's settlement, or a promise - // fulfilled with the plain value (PromiseResolve's rule). - const views = new Map(); - elems.forEach((e, i) => { - if (e.type.kind !== "dyn") return; - const view = lowerer.declareHiddenLocal("%allEntry", dynPromise); - body.push({ - kind: "varDecl", - localId: view.id, - init: { - kind: "libCall", - fn: "dyn.promiseResolve", - args: [ref(i), thenReader(lowerer, loc)], - type: dynPromise, - loc, - }, - loc, - }); - views.set(i, varRef(view.id, dynPromise, loc)); - }); - const widened: IrExpr[] = []; - elems.forEach((e, i) => { - const view = views.get(i); - if (view !== undefined) { - widened.push({ - kind: "promiseVoidWiden", - value: view, - type: { kind: "promise", inner: VOID }, - loc, - }); - return; - } - if (e.type.kind !== "promise") return; - widened.push( - e.type.inner.kind === "void" - ? ref(i) - : { - kind: "promiseVoidWiden", - value: ref(i), - type: { kind: "promise", inner: VOID }, + const shape = lowerer.shapes.get(tuple.shapeId)!; + const entriesRef = varRef("entries.0", entriesT, loc); + // Shape fields are sorted by name ("10" before "2"); a tuple field's + // name is its position. + const fields = shape.fields.map((field) => { + const i = Number(field.name); + const type = elems[i]!.type; + const inner = type.kind === "promise" ? type.inner : type; + const value: IrExpr = + inner.kind === "void" + ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } + : isUnitType(inner) + ? { + kind: "unitLit", + unit: inner.kind === "nullT" ? "null" : "undefined", + type: inner, loc, - }, - ); - }); - const settled: IrExpr = { - kind: "awaitExpr", - value: { - kind: "intrinsic", - name: "promise.all", - args: [ - { - kind: "arrayLit", - elems: widened, - type: arrayOf({ kind: "promise", inner: VOID }), - loc, - }, - ], - type: { kind: "promise", inner: VOID }, - loc, - }, - type: VOID, - loc, - }; - // Shape fields are sorted by name ("10" before "2"); a tuple field's - // name is its position. - const fields = shape.fields.map((field) => { - const i = Number(field.name); - const entry = elems[i]!; - let value: IrExpr; - const view = views.get(i); - if (view !== undefined) { - value = { kind: "awaitExpr", value: view, type: DYN, loc }; - } else if (entry.type.kind === "promise") { - value = - entry.type.inner.kind === "void" - ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } - : { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc }; - } else { - value = ref(i); - } - return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; - }); - body.push( - { kind: "exprStmt", expr: settled, loc }, - { - kind: "return", - value: { kind: "recordLit", fields, type: resultT.inner, loc }, - loc, - }, - ); - lowerer.liftedFns.push({ - name, - params, - returnType: resultT.inner, - locals: [ - ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), - ...lowerer.ctx.locals, - ], - body, - async: true, - loc, - }); - } catch (e) { - lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`); - throw e; - } finally { - lowerer.fnStack.pop(); - } + } + : { + kind: "libCall", + fn: "promise.fulfilledValue", + args: [ + { + kind: "arrayGet", + arr: entriesRef, + index: { kind: "numLit", value: i, type: F64, loc }, + type: voidPromise, + loc, + }, + ], + type: inner, + loc, + }; + return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "entries.0", name: "entries", type: entriesT }], + returnType: tuple, + locals: [{ id: "entries.0", name: "entries", type: entriesT, mutable: false }], + body: [{ kind: "return", value: { kind: "recordLit", fields, type: tuple, loc }, loc }], + loc, + }); } - return { kind: "call", callee: name, args: elems, type: resultT, loc }; + return { + kind: "libCall", + fn: "promise.allTuple", + args: [ + { kind: "arrayLit", elems: entries, type: entriesT, loc }, + { kind: "closure", fnName: name, captures: [], type: funcOf([entriesT], tuple), loc }, + ], + type: resultT, + loc, + }; } /** `Promise.race([...])` on THE Promise global: the entries lower diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..d44f3ebbf 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3139,6 +3139,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // or an optional Error | null slot (same success shape as fs.rename). "process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, "process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, + /** The value of a FULFILLED promise, read without a microtask turn (+1 + * for references): the tuple form of Promise.all reads its entries this + * way once the combinator fulfills, each entry widened to Promise + * and read at the type it was built with (program-dependent). */ + "promise.fulfilledValue": { argTypes: [null], result: VOID }, + /** Promise.all's tuple form as a value: the entries (each widened to + * Promise) join the runtime combinator, and when it fulfills the + * function reads the fulfilled entries into the tuple inside that + * settlement, so the result settles on the combinator's turn; a + * rejection settles it the same way. The result is the tuple's promise + * (program-dependent). */ + "promise.allTuple": { argTypes: [null, null], result: VOID }, "fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, "fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, /** fs.promises.writeFile(path, data, { mode }): the settled-promise diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..08c237fe2 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5310,6 +5310,32 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { + if (e.fn === "promise.fulfilledValue") { + const promise = e.args[0]?.type; + if ( + promise?.kind !== "promise" || + e.type.kind === "void" || + isUnitType(e.type) || + promise.inner.kind !== "void" + ) + err(`libCall ${e.fn} must read a Promise entry at a non-void type`, e.loc); + return true; + } + if (e.fn === "promise.allTuple") { + const [entries, build] = e.args; + if ( + entries?.type.kind !== "array" || + entries.type.elem.kind !== "promise" || + entries.type.elem.inner.kind !== "void" || + build?.kind !== "closure" || + build.captures.length > 0 || + build.type.kind !== "func" || + e.type.kind !== "promise" || + !typeEquals(build.type.ret, e.type.inner) + ) + err(`libCall ${e.fn} must join Promise entries into its builder's tuple`, e.loc); + return true; + } if ( e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest" || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json new file mode 100644 index 000000000..29b935e93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7035-promise-all-tuple-turns.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..5bdfe87a5 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1839,6 +1839,12 @@ void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src) { scr_arr_set_ref(a, i, scr_promise_payload_ref(src)); } +/* The entry promise itself: Promise.all's tuple form reads each fulfilled + * entry at its own type once the combinator fulfills. */ +void scr_promise_all_store_promise(ScrArr *a, double i, ScrPromise *src) { + scr_arr_set_ref(a, i, scr_promise_retain(src)); +} + /* Fulfillment payload accessors for the emitted race adapters — all * retained/by-value (the source keeps its settlement). */ double scr_promise_payload_f64(ScrPromise *p) { return p->f64; } diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..cc79ee1a1 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -369,6 +369,8 @@ void scr_promise_all_store_f64(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_bool(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_str(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src); +/* Stores the fulfilled entry promise itself (+1), for Promise.all's tuple form. */ +void scr_promise_all_store_promise(ScrArr *a, double i, ScrPromise *src); void scr_promise_adapt_copy(ScrPromise *dst, ScrPromise *src); double scr_promise_payload_f64(ScrPromise *p); bool scr_promise_payload_bool(ScrPromise *p); diff --git a/tests/corpus/7035-promise-all-tuple-turns.ts b/tests/corpus/7035-promise-all-tuple-turns.ts new file mode 100644 index 000000000..6989df9b9 --- /dev/null +++ b/tests/corpus/7035-promise-all-tuple-turns.ts @@ -0,0 +1,64 @@ +// Promise.all over entries of different types settles, and resumes its +// awaits and then callbacks, on the same microtask turns as Node, whether +// the result is awaited at once, stored first, chained with then, or +// returned from an async function. The first rejection in settlement +// order wins, later ones count as handled, and record entries keep their +// identity. +const log: string[] = []; +function later(label: string, turns: number, value: T, fail = false): Promise { + return new Promise((resolve, reject) => { + let left = turns; + const step = (): void => { + if (left-- > 0) return queueMicrotask(step); + log.push(`settle ${label}`); + if (fail) reject(new Error(label)); + else resolve(value); + }; + step(); + }); +} +async function num(): Promise { + return 1; +} +async function text(): Promise { + return "s"; +} +async function both(): Promise<[number, string]> { + return Promise.all([num(), text()]); +} +interface Item { + id: number; +} +const item: Item = { id: 7 }; + +async function main(): Promise { + let ticking = true; + void (async () => { + for (let i = 0; i < 40 && ticking; i++) { + log.push(`t${i}`); + await null; + } + })(); + const [a, b] = await Promise.all([num(), text()]); + log.push(`awaited ${a}${b}`); + const stored = Promise.all([text(), num(), Promise.resolve(item)]); + const [c, d, e] = await stored; + log.push(`stored ${c}${d} ${e === item}`); + Promise.all([text(), num()]).then(([f, g]) => log.push(`then ${f}${g}`)); + const [h, i] = await both(); + log.push(`returned ${h}${i}`); + const mixed = await Promise.all([later("x", 3, "x"), 5, later("y", 1, true), undefined]); + log.push(`mixed ${mixed[0]} ${mixed[1]} ${mixed[2]} ${mixed[3]}`); + const raw: any = JSON.parse('{"value":41}'); + const [dynamic, counted] = await Promise.all([raw.value, later("z", 2, 2)]); + log.push(`dynamic ${dynamic + counted}`); + try { + await Promise.all([later("late", 4, 1, true), later("early", 2, "e", true), num()]); + } catch (err) { + log.push(`rejected ${(err as Error).message}`); + } + await later("tail", 6, 0); + ticking = false; + console.log(log.join("\n")); +} +void main(); diff --git a/tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json b/tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json new file mode 100644 index 000000000..9e7986c22 --- /dev/null +++ b/tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json @@ -0,0 +1 @@ +{ "plain": 2910, "san": 4860 } From d14c2ee05f7c6cb720ef6721433df5351ab2f4b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:59:13 -0500 Subject: [PATCH 03/45] Read AsyncLocalStorage options as JavaScript does and call snapshot callbacks without a receiver new AsyncLocalStorage(options) read defaultValue and name as own data properties, so inherited values and getters were ignored, and a name that was not a string reached the compiled code unconverted. The options are now read with a full property get (prototypes, getters, proxies, the snapshot of a typed record or class instance) in Node's order: defaultValue once, name to see whether it is defined and again for its value. The name is kept as String(name); a Symbol throws Node's TypeError, and an exception from a getter propagates. A class instance whose class itself declares one of the two members, which a capsule does not expose to the runtime, is refused with a "not supported yet" error. A snapshot's runner called its callback with the runner's own receiver; Node applies the callback to an undefined receiver. --- packages/runtime/src/scr_async_dyn.c | 70 +++++++++++++++---- .../corpus/7111-als-bind-snapshot-options.ts | 7 ++ .../corpus/7112-als-bind-snapshot-dynamic.mjs | 43 ++++++++++++ 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index c47626557..58a4ac3f8 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -71,17 +71,61 @@ double scr_als_new(void) { return ++scr_als_counter; } +/* options[key] as JavaScript reads it (+1, or NULL with the exception + * pending): inherited properties and getters included. A class instance + * reads its own fields through its snapshot; a member its class declares + * (a getter or a method) has no runtime view here and is refused. */ +static ScrDyn *scr_als_option(ScrDyn *options, const char *key, size_t key_len) { + ScrStr *name = scr_str_new(key, key_len); + if (options->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_has(options, name)) { + char message[160]; + snprintf(message, sizeof message, + "AsyncLocalStorage options whose class declares '%s' are not supported yet", key); + scr_str_release(name); + scr_throw_error_msg(SCR_ERR_ERROR, message, strlen(message)); + return NULL; + } + ScrDyn *key_value = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *value = scr_dyn_reflect_get(options, key_value, options); + scr_dyn_release(key_value); + if (value == NULL && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); + return value; +} + /* new AsyncLocalStorage(options): Node validates that options is an - * object, then keeps options.defaultValue and options.name. NaN with the - * exception pending when options is not an object. */ + * object, then reads options.defaultValue once and options.name twice + * (whether it is defined, then its value), keeping String(name). NaN with + * the exception pending when options is not an object or a read throws. */ double scr_als_new_options(const ScrDyn *options) { if (options->kind == SCR_DYN_UNDEF) return scr_als_new(); - /* A typed record or class instance reads through its snapshot. */ - ScrDyn *snapshot = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL; - if (snapshot) options = snapshot; - if (options->kind != SCR_DYN_OBJ) { + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_TYPED_REF && options->kind != SCR_DYN_PROXY) { scr_dyn_arg_type_fail("options", "of type object", options); - scr_dyn_release(snapshot); + return NAN; + } + ScrDyn *value = scr_als_option((ScrDyn *)options, "defaultValue", 12); + if (!value) return NAN; + ScrDyn *name = scr_als_option((ScrDyn *)options, "name", 4); + if (name && name->kind != SCR_DYN_UNDEF) { + scr_dyn_release(name); + name = scr_als_option((ScrDyn *)options, "name", 4); + if (name && name->kind == SCR_DYN_SYMBOL) { + static const char message[] = "Cannot convert a Symbol value to a string"; + scr_dyn_release(name); + name = NULL; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } else if (name) { + ScrStr *text = scr_dyn_string_coerce_js(name); + scr_dyn_release(name); + name = text ? scr_dyn_new_str(text) : NULL; + scr_str_release(text); + } + } else { + scr_dyn_release(name); + name = scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + if (!name) { + scr_dyn_release(value); return NAN; } double id = scr_als_new(); @@ -94,12 +138,9 @@ double scr_als_new_options(const ScrDyn *options) { scr_als_options = grown; scr_als_options_len = len; } - ScrDyn *value = scr_dyn_obj_get(options, "defaultValue", 12); - ScrDyn *name = scr_dyn_obj_get(options, "name", 4); - scr_als_options[index].default_value = scr_dyn_retain(value ? value : scr_dyn_undefined()); - scr_als_options[index].name = - name && name->kind != SCR_DYN_UNDEF ? scr_dyn_retain(name) : NULL; - scr_dyn_release(snapshot); + scr_als_options[index].default_value = value; /* the read's +1 moves */ + scr_als_options[index].name = name->kind != SCR_DYN_UNDEF ? name : NULL; + if (name->kind == SCR_DYN_UNDEF) scr_dyn_release(name); return id; } @@ -235,7 +276,10 @@ static ScrDyn *scr_als_snapshot_call(ScrClosure *self, ScrDyn *const *args, size ScrAlsCtx **slot = SCR_ALS_SLOT(); ScrAlsCtx *prev = *slot; *slot = scr_box_get_ref(self->caps[0]); /* the slot owns this reference */ + /* Node applies cb to an undefined receiver, whatever the runner's own. */ + scr_dyn_this_push_dyn(scr_dyn_undefined()); ScrDyn *result = scr_dyn_call(cb, args + 1, argc - 1, "callback"); + scr_dyn_this_pop(); scr_als_restore(prev); return result; } diff --git a/tests/corpus/7111-als-bind-snapshot-options.ts b/tests/corpus/7111-als-bind-snapshot-options.ts index d1c6dbbf2..cad93cd5c 100644 --- a/tests/corpus/7111-als-bind-snapshot-options.ts +++ b/tests/corpus/7111-als-bind-snapshot-options.ts @@ -48,6 +48,13 @@ const unnamed = new AsyncLocalStorage(); console.log(JSON.stringify(unnamed.name), unnamed.getStore()); const empty = new AsyncLocalStorage({}); console.log(JSON.stringify(empty.name), empty.getStore()); +// A class instance's fields are options too. +class StoreOptions { + defaultValue = 7; + name = "from-class"; +} +const fromClass = new AsyncLocalStorage(new StoreOptions()); +console.log(fromClass.name, fromClass.getStore()); // Bound functions and snapshots carry the context across asynchronous work. const later = store.run("timer", () => AsyncLocalStorage.bind(() => store.getStore())); diff --git a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs index 22e3238b8..1e20aefd3 100644 --- a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs +++ b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs @@ -43,6 +43,49 @@ console.log(named.name, named.getStore()); named.run(5, () => console.log(named.getStore())); console.log(new AsyncLocalStorage(undefined).name === ""); +// The options are read as JavaScript reads properties: inherited values +// and getters count, defaultValue is read once and name twice, and the +// name becomes a string. +const inherited = new AsyncLocalStorage(Object.create({ defaultValue: "inherited", name: "proto" })); +console.log(inherited.getStore(), inherited.name); +const reads = []; +const viaGetters = new AsyncLocalStorage({ + get defaultValue() { + reads.push("defaultValue"); + return "getter"; + }, + get name() { + reads.push("name"); + return "getter-name"; + }, +}); +console.log(viaGetters.getStore(), viaGetters.name, reads.join(",")); +const coerced = [5, null, { toString: () => "object" }].map((name) => new AsyncLocalStorage({ name }).name); +console.log(JSON.stringify(coerced)); +for (const options of [ + { + get defaultValue() { + throw new Error("from a getter"); + }, + }, + { name: Symbol("s") }, +]) { + try { + new AsyncLocalStorage(options); + console.log("constructed"); + } catch (error) { + console.log(error.name, error.message); + } +} + +// A snapshot calls its callback with an undefined receiver, also when the +// snapshot itself is called as a method. +const kind = function () { + return this === undefined ? "undefined" : typeof this; +}; +const holder = { snap, tag: "holder" }; +console.log(snap(kind), holder.snap(kind), snap.call({ tag: "receiver" }, kind)); + // An untyped callback runs in the stores of its call. store.run("readFile", () => readFile(import.meta.filename, "utf8", (error, text) => From de6dd133708f11851621487aa6ddc49c9252666c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:45:31 -0500 Subject: [PATCH 04/45] Run net, tls, http and https events in Node's AsyncLocalStorage contexts The runtime fired every socket, server, request and response event from the event loop with no store. Node runs them in the context of the resource or request that produced them, so getStore() answered undefined in listeners where Node answers the caller's store. Handles now keep the context Node uses and the runtime enters it around each dispatch: - a socket the context of its creation (net.connect, tls.connect, an http request's socket), for 'connect', 'data', 'end', 'close', 'error' and 'timeout'; - a server the contexts of its listen() call ('listening', 'connection', listen errors) and its close() call ('close' and close callbacks when no connection is open; a close that waits for the last connection settles in that connection's dispatch, as in Node); - socket write() callbacks and end() callbacks with 'finish' the contexts of their calls; - http server requests and responses, which are created in their connection's dispatch, the server's listen() context, and client requests and their responses the http.request() context, including a keep-alive socket an agent hands to a later request. An accepted net socket runs its own events with no store, as in Node; the sockets of an HTTP or TLS server run in the server's listen() context. Events and callbacks of other streams are unchanged. The compiler is unchanged. Programs without net or http link the same runtime code; net and http programs gain the capture and dispatch helpers (about 1.7 KB of text in a net program, 2.2 KB in an http server), and bench:runtime http-api is neutral. Corpus 7119 and 7380. --- .../builtin-apis/asynclocalstorage.mdx | 5 +- .../generated/node-v24-internal.json | 34 ++- internal/compatibility/static-support.json | 2 +- .../7119-als-net-event-context.mjs.json | 6 + .../7380-als-http-event-context.mjs.json | 6 + packages/runtime/src/scr_async.c | 11 + packages/runtime/src/scr_async.h | 9 + packages/runtime/src/scr_http.c | 40 +++ packages/runtime/src/scr_net.c | 252 +++++++++++++----- packages/runtime/src/scr_net.h | 4 + tests/corpus/7119-als-net-event-context.mjs | 124 +++++++++ tests/corpus/7380-als-http-event-context.mjs | 151 +++++++++++ .../7119-als-net-event-context.mjs.json | 1 + .../7380-als-http-event-context.mjs.json | 1 + 14 files changed, 573 insertions(+), 73 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json create mode 100644 tests/corpus/7119-als-net-event-context.mjs create mode 100644 tests/corpus/7380-als-http-event-context.mjs create mode 100644 tests/harness/corpus-costs/7119-als-net-event-context.mjs.json create mode 100644 tests/harness/corpus-costs/7380-als-http-event-context.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 99f89d65c..98fc92ea1 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -1,4 +1,7 @@ - **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node. - Event listeners on sockets, requests, responses and streams, stream callbacks such as `write(chunk, callback)`, `server.close(callback)`, and the callbacks of `https` and `tls` calls run with no store, where Node runs them in the context of the resource or of the code that triggers them. + Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with. + A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call. + Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call. + Events and callbacks of other streams run with no store, where Node runs them in the context of the code that triggers them. `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 34fe2a843..6046daf0e 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1390,7 +1390,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "chapter" @@ -1443,7 +1445,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1519,7 +1523,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1549,7 +1555,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1579,7 +1587,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1609,7 +1619,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1639,7 +1651,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1946,6 +1960,8 @@ "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2511,7 +2527,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index efc60f263..c57d6013e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json new file mode 100644 index 000000000..7ff81ea34 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7119-als-net-event-context.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json new file mode 100644 index 000000000..1087d25fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-als-http-event-context.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 992ec3767..90e7dd04f 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -774,6 +774,17 @@ void scr_als_restore(ScrAlsCtx *prev) { *SCR_ALS_SLOT() = prev; /* ownership moves back in */ } +ScrAlsCtx *scr_als_dispatch_enter(ScrAlsCtx *ctx) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_als_ctx_retain(ctx); + return prev; +} + +void scr_als_dispatch_leave(ScrAlsCtx *prev) { scr_als_restore(prev); } + +ScrAlsCtx *scr_als_capture(void) { return scr_als_ctx_retain(*SCR_ALS_SLOT()); } + /* The value of `id` in the active snapshot (borrowed), or NULL. */ ScrDyn *scr_als_peek(double id) { const ScrAlsCtx *c = *SCR_ALS_SLOT(); diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 003bf3be8..25f25a842 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -192,6 +192,15 @@ ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ ScrAlsCtx *scr_als_enter_absent(double id); /* exit()'s cleared arm */ void scr_als_restore(ScrAlsCtx *prev); /* moves prev back in */ +/* A runtime dispatch (an I/O event the loop delivers, a deferred emit) + * runs its listeners in the AsyncLocalStorage context of the object or + * request Node would run them in: enter installs `ctx` (borrowed, NULL + * for no store) and answers the previous context, which leave puts back. + * A store a listener enters (enterWith) ends with the dispatch. */ +ScrAlsCtx *scr_als_dispatch_enter(ScrAlsCtx *ctx); +void scr_als_dispatch_leave(ScrAlsCtx *prev); +/* The active context, +1 (NULL outside a store): what a handle keeps. */ +ScrAlsCtx *scr_als_capture(void); void scr_als_enter_with(double id, ScrDyn *value); /* no restore point (enterWith) */ void scr_als_disable(double id); /* run/exit: enter (or clear), call the dyn function with the forwarded diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index a5847e59b..18d997fb5 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -209,6 +209,11 @@ void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void struct ScrHttpReq { size_t rc; + /* The AsyncLocalStorage context of its creation (+1, NULL outside a + * store): the deferred emits run in it (scr_http_proto_sweep). A server + * request is created in its connection's dispatch, which runs in the + * server's listen() context; a client response in its request's. */ + ScrAlsCtx *als; int status; /* -1 on server requests (Node's undefined statusCode) */ ScrStr *method; ScrStr *url; @@ -301,6 +306,7 @@ void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/) { void scr_http_req_release(ScrHttpReq *r) { if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { + scr_als_ctx_release(r->als); scr_str_release(r->method); scr_str_release(r->url); for (size_t i = 0; i < r->nheaders; i++) { @@ -928,6 +934,7 @@ static bool scr_http_trailers_replace(ScrHttpTrailers *t, ScrArr *pairs /*borrow struct ScrHttpRes { size_t rc; + ScrAlsCtx *als; /* +1: as ScrHttpReq's */ ScrNetSocket *sock; /* +1; writes after the connection died are no-ops */ void *h2_stream; /* +1 through scr_http_h2_ops — set on h2 compat * responses; every write path routes to frames */ @@ -985,6 +992,7 @@ ScrHttpRes *scr_http_res_retain(ScrHttpRes *r) { void scr_http_res_release(ScrHttpRes *r) { if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { + scr_als_ctx_release(r->als); for (size_t i = 0; i < r->nheaders; i++) { scr_str_release(r->hnames[i]); scr_str_release(r->hvalues[i]); @@ -2257,9 +2265,25 @@ void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/) { static bool scr_http_proto_pending(void) { return scr_http_emits_head < scr_http_emits_len; } +static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c); + +/* The context an emit's handle was created in (borrowed). */ +static ScrAlsCtx *scr_http_emit_ctx(const ScrHttpEmit *e) { + switch (e->kind) { + case SCR_HTTP_EMIT_RES_CLOSE: + case SCR_HTTP_EMIT_RES_WCB: + case SCR_HTTP_EMIT_RES_FINISH: return ((ScrHttpRes *)e->h)->als; + case SCR_HTTP_EMIT_REQ_CLOSE: + case SCR_HTTP_EMIT_REQ_DRAIN: + case SCR_HTTP_EMIT_REQ_ABORTED: return ((ScrHttpReq *)e->h)->als; + default: return scr_http_client_ctx((struct ScrHttpClientReq *)e->h); + } +} + static void scr_http_proto_sweep(void) { while (scr_http_emits_head < scr_http_emits_len) { ScrHttpEmit e = scr_http_emits[scr_http_emits_head++]; + ScrAlsCtx *prev = scr_als_dispatch_enter(scr_http_emit_ctx(&e)); switch (e.kind) { case SCR_HTTP_EMIT_RES_CLOSE: { ScrHttpRes *res = (ScrHttpRes *)e.h; @@ -2349,6 +2373,7 @@ static void scr_http_proto_sweep(void) { break; } } + scr_als_dispatch_leave(prev); scr_http_emit_release(&e); if (scr_exc_pending()) return; } @@ -2870,6 +2895,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o ScrHttpReq *req = calloc(1, sizeof *req); if (!req) scr_http_oom(); req->rc = 1; + req->als = scr_als_capture(); req->status = -1; /* Node: statusCode is undefined on server requests */ req->gated = true; req->join_dup = conn->srv != NULL && conn->srv->join_dup; @@ -3038,6 +3064,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o ScrHttpRes *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_http_live++; #endif @@ -3590,6 +3617,7 @@ ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/, ScrHttpReq *req = calloc(1, sizeof *req); if (!req) scr_http_oom(); req->rc = 1; + req->als = scr_als_capture(); req->status = -1; /* server requests: statusCode is Node's undefined */ req->http2 = true; req->sock = sock ? scr_net_sock_retain(sock) : NULL; @@ -3661,6 +3689,7 @@ ScrHttpRes *scr_http_h2_res_new(ScrNetSocket *sock /*borrowed, nullable*/, ScrHttpRes *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); res->sock = sock ? scr_net_sock_retain(sock) : NULL; res->h2_stream = scr_http_h2_ops->retain(stream); res->keep_alive = true; /* inert on h2 — no Connection header exists */ @@ -3770,6 +3799,9 @@ void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) { struct ScrHttpClientReq { size_t rc; + /* The http.request() call's AsyncLocalStorage context (+1): the + * request's deferred emits ('socket', 'finish', 'close') run in it. */ + ScrAlsCtx *als; ScrNetSocket *sock; /* +1 */ ScrHttpConn *conn; /* borrowed backref (the socket's ctx); NULL after free */ ScrStr *host, *path, *method; @@ -3834,6 +3866,7 @@ ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c) { void scr_http_client_release(ScrHttpClientReq *c) { if (!c || c->rc == SIZE_MAX) return; if (--c->rc == 0) { + scr_als_ctx_release(c->als); scr_net_ls_drop(&c->resp_ls); scr_net_ls_drop(&c->err_ls); scr_net_ls_drop(&c->timeout_ls); @@ -3872,6 +3905,8 @@ static void scr_http_client_release_internal(struct ScrHttpClientReq *c) { scr_http_client_release(c); } +static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c) { return c->als; } + void *scr_http_client_retain_v(void *p) { return scr_http_client_retain((ScrHttpClientReq *)p); } void scr_http_client_release_v(void *p) { scr_http_client_release((ScrHttpClientReq *)p); } @@ -4648,6 +4683,7 @@ static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len) { ScrHttpReq *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); res->status = status; res->sock = scr_net_sock_retain(conn->sock); #ifdef SCR_RC_AUDIT @@ -5041,6 +5077,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double ScrHttpClientReq *c = calloc(1, sizeof *c); if (!c) scr_http_oom(); c->rc = 1; + c->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_http_live++; #endif @@ -5086,6 +5123,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double if (presock != NULL) { c->sock = presock; + scr_net_sock_set_context(c->sock, c->als); } else { ScrStr *dial = scr_net_blocking_lookup(host); c->sock = scr_net_connect(port, dial, NULL); @@ -5344,6 +5382,8 @@ static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, boo scr_net_sock_release(old); ScrHttpConn *conn = f->conn; c->sock = f->sock; /* the free entry's +1 moves */ + /* Node runs a pooled socket's events for this request in its context. */ + scr_net_sock_set_context(c->sock, c->als); c->conn = conn; conn->client = scr_http_client_retain(c); conn->pool_agent = NULL; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 29a4af393..33374b97c 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -420,6 +420,11 @@ enum { SCR_NET_K_SERVER = 1, SCR_NET_K_SOCKET = 2 }; struct ScrNetServer { int kind; /* FIRST: poller udata routes on it */ size_t rc; + /* The AsyncLocalStorage contexts of the listen() and close() calls + * (+1, NULL outside a store): 'listening', 'connection' and 'error' run + * in the first, 'close' and the close() callbacks in the second, as + * Node runs them in its handle's and its close request's contexts. */ + ScrAlsCtx *listen_ctx, *close_ctx; int fd; /* -1 while not listening */ int port; bool listening; @@ -486,6 +491,14 @@ struct ScrNetServer { struct ScrNetSocket { int kind; /* FIRST: poller udata routes on it */ size_t rc; + /* The AsyncLocalStorage context of the socket's creation (+1, NULL + * outside a store): its I/O events ('connect', 'data', 'end', 'close', + * 'error', 'timeout') run in it, as Node runs them in its handle's + * context. An accepted socket has none, except under an HTTP or TLS + * server, whose sockets take the server's listen() context. end_ctx is + * the end() call's context, for 'finish' and the end() callback (Node's + * shutdown request). */ + ScrAlsCtx *als, *end_ctx; int fd; /* -1 once closed */ bool unrefd; /* socket.unref(): does not keep the loop alive */ bool connecting; @@ -668,6 +681,13 @@ static SCR_TL ScrPoller *scr_net_poller = NULL; static void scr_net_close_fd_raw(int fd); /* forget-then-close, defined below */ +/* The listen() call's context: the server's 'listening', 'connection' + * and 'error' events run in it. */ +static void scr_net_server_capture_listen(ScrNetServer *s) { + scr_als_ctx_release(s->listen_ctx); + s->listen_ctx = scr_als_capture(); +} + ScrNetServer *scr_net_server_retain(ScrNetServer *s) { if (s->rc != SIZE_MAX) s->rc++; return s; @@ -683,6 +703,8 @@ void scr_net_server_release(ScrNetServer *s) { scr_net_ls_drop(&s->listening_cbs); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->drop_ls); + scr_als_ctx_release(s->listen_ctx); + scr_als_ctx_release(s->close_ctx); scr_closure_release(s->close_override); for (size_t i = 0; i < 5; i++) scr_dyn_release(s->http_timeout_dyn[i]); scr_dyn_release(s->drop_max_connection); @@ -721,6 +743,8 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); scr_net_ls_drop(&s->ready_ls); + scr_als_ctx_release(s->als); + scr_als_ctx_release(s->end_ctx); scr_str_release(s->pending_err); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); @@ -965,6 +989,7 @@ static ScrNetSocket *scr_net_sock_new(void) { s->kind = SCR_NET_K_SOCKET; s->rc = 1; s->fd = -1; + s->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_net_live++; #endif @@ -1675,6 +1700,7 @@ ScrNetServer *scr_net_create_server(ScrClosure *handler /*moves, nullable*/, Scr * listen: IPv6 any with dual-stack when the kernel allows, IPv4 fallback. */ static void scr_net_listen_any(ScrNetServer *s, double port, bool reuse_port, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); if (cb) scr_net_ls_add(&s->listening_cbs, cb, NULL, true); if (s->listening || s->close_emitted) return; if (!scr_net_poller_init()) { @@ -1753,6 +1779,7 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab static void scr_net_listen_opts_impl(ScrNetServer *s, double port, ScrStr *host /*borrowed*/, bool ipv6_only, bool reuse_port, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); if (host == NULL || host->len == 0) { scr_net_listen_any(s, port, reuse_port, cb); return; @@ -1968,6 +1995,8 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab scr_closure_release(cb); return; } + scr_als_ctx_release(s->close_ctx); + s->close_ctx = scr_als_capture(); if (cb) scr_net_ls_add(&s->close_ls, cb, NULL, true); if (s->listening) { scr_net_close_fd_raw(s->fd); /* stops accepting NOW (registrations dropped first) */ @@ -2225,6 +2254,13 @@ static void scr_net_server_accept(ScrNetServer *srv) { continue; } ScrNetSocket *sock = scr_net_sock_new(); + /* Node runs an accepted socket's own events with no store, except + * under the protocol layer (an HTTP or TLS server), whose connections + * run in the listen() context this accept runs in. */ + if (!srv->native_conn && !srv->defer_conn) { + scr_als_ctx_release(sock->als); + sock->als = NULL; + } sock->fd = fd; sock->server = scr_net_server_retain(srv); srv->nconns++; @@ -2776,6 +2812,7 @@ void scr_net_server_address_check(ScrNetServer *s) { * deferred 'error', success the deferred 'listening', as for a port. */ void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); /* Node's isPipeName: a string that reads as a non-negative number is a port */ double port = scr_string_to_number(path); if (port >= 0) { @@ -3078,6 +3115,8 @@ void scr_net_sock_end(ScrNetSocket *s) { } if (s->fd < 0 || s->wr_ending) return; s->wr_ending = true; + scr_als_ctx_release(s->end_ctx); + s->end_ctx = scr_als_capture(); scr_net_sock_maybe_finish_write(s); if (s->rd_eof && s->wr_done) scr_net_sock_close_fd(s); } @@ -3175,6 +3214,62 @@ void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/) { if (s->wr_done) s->finish_pending = true; /* already finished: next sweep */ } +/* The context the socket's events run in from now on (borrowed): an HTTP + * agent hands a pooled socket to another request. */ +void scr_net_sock_set_context(ScrNetSocket *s, ScrAlsCtx *ctx) { + ScrAlsCtx *next = scr_als_ctx_retain(ctx); + scr_als_ctx_release(s->als); + s->als = next; +} + +/* A write callback runs in the write() call's AsyncLocalStorage context + * (Node's write request): with a store active, the list holds a closure + * that enters it around the callback. caps[0] is the callback, caps[1] + * the context. */ +static void *scr_net_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_net_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static void scr_net_ctx_fire0(ScrClosure *self) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *ctx = scr_box_get_ref(self->caps[1]); + ScrAlsCtx *prev = scr_als_dispatch_enter(ctx); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_als_dispatch_leave(prev); + scr_als_ctx_release(ctx); + scr_closure_release(cb); +} + +static void scr_net_dyn_fire_arg(ScrClosure *cb, const ScrDyn *arg); + +static void scr_net_ctx_fire_arg(ScrClosure *self, const ScrDyn *arg) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *ctx = scr_box_get_ref(self->caps[1]); + ScrAlsCtx *prev = scr_als_dispatch_enter(ctx); + scr_net_dyn_fire_arg(cb, arg); + scr_als_dispatch_leave(prev); + scr_als_ctx_release(ctx); + scr_closure_release(cb); +} + +/* Adds a write callback (moves) with its firing adapter (NULL: a + * zero-argument closure; scr_net_dyn_fire_arg: a dyn listener). */ +static __attribute__((noinline)) ScrClosure *scr_net_bind_write_cb(ScrClosure *cb /*moves*/, void **fn, + ScrAlsCtx *active) { + ScrClosure *bound = scr_closure_new(*fn == NULL ? (void *)scr_net_ctx_fire0 : NULL, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_net_ctx_retain_v, scr_net_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + if (*fn != NULL) *fn = (void *)&scr_net_ctx_fire_arg; + return bound; +} + +static void scr_net_sock_add_write_cb(ScrNetSocket *s, ScrClosure *cb, void *fn) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (active != NULL) cb = scr_net_bind_write_cb(cb, &fn, active); + scr_net_ls_add(&s->wcb_ls, cb, fn, true); +} + /* write(chunk, callback): fires when the write buffer drains (this * surface's flush moment), from the sweep. */ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { @@ -3182,7 +3277,7 @@ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { scr_closure_release(cb); return; } - scr_net_ls_add(&s->wcb_ls, cb, NULL, true); + scr_net_sock_add_write_cb(s, cb, NULL); } double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; } @@ -4584,8 +4679,8 @@ void scr_net_server_close_dyn(ScrNetServer *s, const ScrDyn *listener /*borrowed void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { if (s->close_emitted) return; - scr_net_ls_add(&s->wcb_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), - (void *)&scr_net_dyn_fire_arg, true); + scr_net_sock_add_write_cb(s, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg); } /* socket.setTimeout(ms[, callback]) as the program calls it: Node's @@ -5382,7 +5477,9 @@ static void scr_net_sweep(void) { ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) || (!sock->tops && sock->rlen > sock->rhead))) { /* deliver bytes the kernel can't re-signal (see flags_pending) */ + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); scr_net_sock_read(sock); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -5404,14 +5501,18 @@ static void scr_net_sweep(void) { } if (sock->finish_pending) { sock->finish_pending = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->end_ctx); scr_net_fire0_this(&sock->finish_ls, sock, SCR_DYNH_NET_SOCKET); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; } } if (sock->lookup_evs != NULL && sock->pending_err) { + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); scr_net_lookup_ops->flush(sock); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -5425,6 +5526,7 @@ static void scr_net_sweep(void) { * teardown without an event); unconsumed errors fall through to the socket's own * listeners — and the unhandled-'error' exit */ bool consumed = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); if (sock->native_err) consumed = sock->native_err(sock->native_ctx, msg); if (!consumed && !scr_exc_pending()) { ScrError *coded = NULL; @@ -5438,6 +5540,7 @@ static void scr_net_sweep(void) { scr_event_error_payload = saved; if (coded != NULL) scr_error_release(coded); } + scr_als_dispatch_leave(prev); sock->pending_code = NULL; scr_str_release(msg); if (scr_exc_pending()) { @@ -5446,7 +5549,10 @@ static void scr_net_sweep(void) { } } if (sock->emit_close && !sock->close_emitted && sock->fd < 0) { - if (!scr_net_sock_settle(sock)) { + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); + bool settled = scr_net_sock_settle(sock); + scr_als_dispatch_leave(prev); + if (!settled) { scr_net_sock_release(sock); return; } @@ -5460,7 +5566,9 @@ static void scr_net_sweep(void) { scr_net_server_retain(srv); if (srv->emit_listening) { srv->emit_listening = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); scr_net_fire0_this(&srv->listening_cbs, srv, SCR_DYNH_NET_SERVER); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_server_release(srv); return; @@ -5469,7 +5577,9 @@ static void scr_net_sweep(void) { if (srv->pending_err) { ScrStr *msg = srv->pending_err; srv->pending_err = NULL; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); scr_net_fire_err_this(&srv->err_ls, msg, srv, SCR_DYNH_NET_SERVER); + scr_als_dispatch_leave(prev); scr_str_release(msg); /* A failed listen SETTLES the handle: listeners drop (an error * listener capturing its own server would otherwise cycle forever — @@ -5505,7 +5615,9 @@ static void scr_net_sweep(void) { } if (due == NULL) return; scr_net_server_retain(due); + ScrAlsCtx *prev = scr_als_dispatch_enter(due->close_ctx); scr_net_server_settle(due); + scr_als_dispatch_leave(prev); scr_net_server_release(due); if (scr_exc_pending()) return; } @@ -5539,6 +5651,69 @@ bool scr_loop_io_take(void); bool scr_loop_io_more(void); void scr_loop_io_reopen(void); +/* One poller event of a socket; false with an exception pending. */ +static bool scr_net_sock_event(ScrNetSocket *s, unsigned events) { + if (events & SCRP_TIMER) { + /* the idle period elapsed: 'timeout' fires once (one-shot — the + * next activity re-arms); the socket stays open, like Node */ + s->timeout_armed = false; + if (s->native_timeout) { + s->native_timeout(s->native_ctx); + if (scr_exc_pending()) return false; + } + if (s->server && s->server->http_timeout_surface && + s->native_http_timeout) { + ScrNetServer *srv = scr_net_server_retain(s->server); + bool handled = s->native_http_timeout(s->native_ctx); + if (scr_exc_pending()) { + scr_net_server_release(srv); + return false; + } + bool server_handled = srv->timeout_ls.n > 0; + if (server_handled) { + ScrNetL *snap; + size_t count = scr_net_ls_snapshot(&srv->timeout_ls, &snap); + scr_dyn_this_push(srv, SCR_DYNH_NET_SERVER); + for (size_t j = 0; j < count; j++) { + if (!scr_exc_pending()) ((ScrNetConnFn)snap[j].fn)(snap[j].cb, scr_net_sock_retain(s)); + scr_closure_release(snap[j].cb); + } + scr_dyn_this_pop(); + free(snap); + } + if (!handled && !server_handled && !scr_exc_pending()) + scr_net_sock_destroy(s); /* Node's unhandled server timeout */ + scr_net_server_release(srv); + if (scr_exc_pending()) return false; + } + scr_net_fire0_this(&s->timeout_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return false; + if (s->fd < 0) return true; + } + /* kqueue delivers one direction per event (today's exact order); + * epoll may coalesce both into one — writes first (connect + * completion, flushes), then reads, re-checking the fd a write + * failure may have closed. */ + if (events & SCRP_WRITABLE) { + if (s->connecting) scr_net_sock_connect_done(s); + else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); + else if (s->tops && !s->t_est) { + s->t_want_write = false; + scr_net_sock_transport_pump(s); + if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); + } else { + s->t_want_write = false; + scr_net_sock_flush(s); + } + } + if ((events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) { + if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); + else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s); + else scr_net_sock_read(s); + } + return true; +} + static void scr_net_dispatch(void) { if (!scr_net_servers && !scr_net_socks && !scr_net_proto_pending()) { @@ -5564,71 +5739,22 @@ static void scr_net_dispatch(void) { if (!udata) continue; int kind = *(int *)udata; if (kind == SCR_NET_K_SERVER) { - scr_net_server_accept((ScrNetServer *)udata); + /* 'connection' runs in the context of the listen() call. */ + ScrNetServer *srv = (ScrNetServer *)udata; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); + scr_net_server_accept(srv); + scr_als_dispatch_leave(prev); } else if (kind == SCR_NET_K_SOCKET) { ScrNetSocket *s = (ScrNetSocket *)udata; /* A callback earlier in this batch may have closed this fd * (destroy inside a data listener) — its remaining events are * stale; the handle itself stays alive until the sweep. */ if (s->fd < 0) continue; - if (evs[i].events & SCRP_TIMER) { - /* the idle period elapsed: 'timeout' fires once (one-shot — the - * next activity re-arms); the socket stays open, like Node */ - s->timeout_armed = false; - if (s->native_timeout) { - s->native_timeout(s->native_ctx); - if (scr_exc_pending()) return; - } - if (s->server && s->server->http_timeout_surface && - s->native_http_timeout) { - ScrNetServer *srv = scr_net_server_retain(s->server); - bool handled = s->native_http_timeout(s->native_ctx); - if (scr_exc_pending()) { - scr_net_server_release(srv); - return; - } - bool server_handled = srv->timeout_ls.n > 0; - if (server_handled) { - ScrNetL *snap; - size_t count = scr_net_ls_snapshot(&srv->timeout_ls, &snap); - scr_dyn_this_push(srv, SCR_DYNH_NET_SERVER); - for (size_t j = 0; j < count; j++) { - if (!scr_exc_pending()) ((ScrNetConnFn)snap[j].fn)(snap[j].cb, scr_net_sock_retain(s)); - scr_closure_release(snap[j].cb); - } - scr_dyn_this_pop(); - free(snap); - } - if (!handled && !server_handled && !scr_exc_pending()) - scr_net_sock_destroy(s); /* Node's unhandled server timeout */ - scr_net_server_release(srv); - if (scr_exc_pending()) return; - } - scr_net_fire0_this(&s->timeout_ls, s, SCR_DYNH_NET_SOCKET); - if (scr_exc_pending()) return; - if (s->fd < 0) continue; - } - /* kqueue delivers one direction per event (today's exact order); - * epoll may coalesce both into one — writes first (connect - * completion, flushes), then reads, re-checking the fd a write - * failure may have closed. */ - if (evs[i].events & SCRP_WRITABLE) { - if (s->connecting) scr_net_sock_connect_done(s); - else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); - else if (s->tops && !s->t_est) { - s->t_want_write = false; - scr_net_sock_transport_pump(s); - if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); - } else { - s->t_want_write = false; - scr_net_sock_flush(s); - } - } - if ((evs[i].events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) { - if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); - else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s); - else scr_net_sock_read(s); - } + /* The socket's events run in its AsyncLocalStorage context. */ + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + bool ok = scr_net_sock_event(s, evs[i].events); + scr_als_dispatch_leave(prev); + if (!ok) return; } if (scr_exc_pending()) return; } diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index ffa9b2f72..d31168a33 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -36,6 +36,10 @@ typedef struct { } ScrNetLs; void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once); +/* The AsyncLocalStorage context a socket's events run in from now on + * (borrowed; scr_net.c keeps the creation context otherwise). */ +struct ScrAlsCtx; +void scr_net_sock_set_context(ScrNetSocket *s, struct ScrAlsCtx *ctx); ScrNetOnce *scr_net_once_new(void); /* +1 */ ScrNetOnce *scr_net_once_retain(ScrNetOnce *once); /* +1 */ void scr_net_once_release(ScrNetOnce *once); diff --git a/tests/corpus/7119-als-net-event-context.mjs b/tests/corpus/7119-als-net-event-context.mjs new file mode 100644 index 000000000..3fb95ffa8 --- /dev/null +++ b/tests/corpus/7119-als-net-event-context.mjs @@ -0,0 +1,124 @@ +// AsyncLocalStorage contexts of node:net events. A client socket's events +// run in the context of the net.connect() call, whatever context the +// listener was registered in, also after a destroy() from elsewhere. The +// server's 'listening', 'connection' and 'error' events run in the +// context of the listen() call. An accepted socket's own events run with +// no store. write() and end() callbacks, and 'finish', run in the context +// of their call. server.close(callback) runs in the close() call's +// context when no connection is open, and with no store when it waits for +// the last connection. Lines are sorted at the end: events on different +// sockets may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import net from "node:net"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +function echoScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => + net.createServer((sock) => { + log("server connection callback"); + sock.on("data", () => { + log("accepted data"); + store.run("in-data", () => sock.write("pong")); + }); + sock.on("end", () => { + log("accepted end"); + sock.end(); + }); + sock.on("close", () => log("accepted close")); + }), + ); + server.on("connection", () => log("server connection event")); + server.on("close", () => { + log("server close event"); + resolve(); + }); + store.run("listen", () => + server.listen(0, () => { + log("listen callback"); + const client = store.run("connect", () => net.connect(server.address().port)); + store.run("register", () => { + client.on("connect", () => { + log("client connect"); + store.run("write", () => client.write("ping", () => log("write callback"))); + }); + client.on("data", () => { + log("client data"); + store.run("end-call", () => client.end(() => log("end callback"))); + }); + client.on("finish", () => log("client finish")); + client.on("end", () => log("client end")); + client.on("close", () => { + log("client close"); + store.run("close-call", () => server.close(() => log("server close callback"))); + }); + }); + }), + ); + }); +} + +function destroyScenario() { + return new Promise((resolve) => { + const server = net.createServer((sock) => sock.on("data", () => {})); + server.listen(0, () => { + const client = store.run("connect-2", () => net.connect(server.address().port)); + client.on("error", () => {}); + client.on("connect", () => { + client.on("close", () => { + log("close after a destroy elsewhere"); + server.close(() => resolve()); + }); + setTimeout(() => store.run("destroy-call", () => client.destroy()), 10); + }); + }); + }); +} + +function closeScenarios() { + return new Promise((resolve) => { + let pending = 3; + const done = () => { + if (--pending === 0) resolve(); + }; + const waiting = net.createServer((sock) => { + store.run("conn", () => waiting.close(() => { + log("close callback that waited"); + done(); + })); + setTimeout(() => store.run("end", () => sock.end()), 20); + }); + store.run("listen-3", () => + waiting.listen(0, () => { + const client = net.connect(waiting.address().port); + client.on("end", () => client.end()); + }), + ); + const idle = net.createServer(); + store.run("listen-4", () => + idle.listen(0, () => { + store.run("close-now", () => idle.close(() => { + log("close callback of an idle server"); + done(); + })); + }), + ); + const taken = net.createServer(); + taken.listen(0, () => { + const clash = net.createServer(); + clash.on("error", (e) => { + log(`listen error ${e.code}`); + taken.close(() => done()); + }); + store.run("clash-listen", () => clash.listen(taken.address().port)); + }); + }); +} + +await echoScenario(); +await destroyScenario(); +await closeScenarios(); +for (const line of lines.sort()) console.log(line); diff --git a/tests/corpus/7380-als-http-event-context.mjs b/tests/corpus/7380-als-http-event-context.mjs new file mode 100644 index 000000000..a881cbe7e --- /dev/null +++ b/tests/corpus/7380-als-http-event-context.mjs @@ -0,0 +1,151 @@ +// AsyncLocalStorage contexts of node:http, node:https and node:tls events. +// A server's 'request' event and its requests' and responses' events run +// in the context of the server's listen() call. A client request's +// events, its response callback and its response's events run in the +// context of the http.request() call, also on a keep-alive socket that +// an earlier request used. TLS client sockets run in the tls.connect() +// call's context, and a TLS server's connections in its listen() +// context. Lines are sorted at the end: events on different sockets may +// interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import * as fs from "node:fs"; +import http from "node:http"; +import https from "node:https"; +import tls from "node:tls"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +function httpScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => + http.createServer((req, res) => { + log("request callback"); + req.on("data", () => log("request data")); + req.on("end", () => { + log("request end"); + res.end("ok"); + }); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + }), + ); + server.on("request", () => log("request event")); + store.run("listen", () => + server.listen(0, () => { + const port = server.address().port; + const req = store.run("request-call", () => + http.request({ port, method: "POST" }, (res) => { + log("response callback"); + res.on("data", () => log("client response data")); + res.on("end", () => log("client response end")); + }), + ); + store.run("register", () => { + req.on("socket", () => log("client socket")); + req.on("finish", () => log("client finish")); + req.on("close", () => { + log("client close"); + server.close(() => resolve()); + }); + }); + store.run("end-call", () => req.end("body")); + }), + ); + }); +} + +function keepAliveScenario() { + return new Promise((resolve) => { + const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const server = http.createServer((req, res) => res.end("ok")); + server.listen(0, () => { + const port = server.address().port; + let done = 0; + for (const name of ["first", "second"]) { + store.run(name, () => { + const req = http.get({ port, agent }, (res) => { + log(`${name} response`); + res.on("data", () => {}); + res.on("end", () => { + log(`${name} response end`); + if (++done === 2) { + agent.destroy(); + server.close(() => resolve()); + } + }); + }); + req.on("finish", () => log(`${name} finish`)); + req.on("close", () => log(`${name} close`)); + }); + } + }); + }); +} + +const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); +const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); + +function httpsScenario() { + return new Promise((resolve) => { + const server = https.createServer({ key, cert }, (req, res) => { + log("https request"); + req.resume(); + res.on("finish", () => log("https response finish")); + res.end("ok"); + }); + store.run("https-listen", () => + server.listen(0, () => { + const port = server.address().port; + store.run("https-get", () => + https + .get({ port, host: "localhost", ca }, (res) => { + log("https response"); + res.on("data", () => {}); + res.on("end", () => log("https response end")); + }) + .on("close", () => { + log("https client close"); + server.close(() => resolve()); + }), + ); + }), + ); + }); +} + +function tlsScenario() { + return new Promise((resolve) => { + const server = tls.createServer({ key, cert }, (sock) => { + log("tls secureConnection"); + sock.on("data", () => { + log("tls server data"); + sock.end("bye"); + }); + }); + store.run("tls-listen", () => + server.listen(0, () => { + const client = store.run("tls-connect", () => + tls.connect({ port: server.address().port, host: "localhost", ca }, () => { + log("tls secureConnect"); + client.write("hi"); + }), + ); + client.on("data", () => log("tls client data")); + client.on("end", () => log("tls client end")); + client.on("close", () => { + log("tls client close"); + server.close(() => resolve()); + }); + }), + ); + }); +} + +await httpScenario(); +await keepAliveScenario(); +await httpsScenario(); +await tlsScenario(); +for (const line of lines.sort()) console.log(line); diff --git a/tests/harness/corpus-costs/7119-als-net-event-context.mjs.json b/tests/harness/corpus-costs/7119-als-net-event-context.mjs.json new file mode 100644 index 000000000..a57dddb65 --- /dev/null +++ b/tests/harness/corpus-costs/7119-als-net-event-context.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 12000 } diff --git a/tests/harness/corpus-costs/7380-als-http-event-context.mjs.json b/tests/harness/corpus-costs/7380-als-http-event-context.mjs.json new file mode 100644 index 000000000..a57dddb65 --- /dev/null +++ b/tests/harness/corpus-costs/7380-als-http-event-context.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 12000 } From 74fcda0ad93deb463249c9a61c244f5e9d57ded2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:17:42 -0500 Subject: [PATCH 05/45] Evaluate every Promise.all entry before a dynamic entry's then lookup A checked-dynamic entry's PromiseResolve reads its `then` property, which can be a getter. The tuple form resolved each entry as the array was built, so that getter ran before later entries evaluated; Node evaluates the whole array first. With a checked-dynamic entry, every entry now evaluates into a hidden local before the entries resolve. --- .../frontend/lowering/builtins/promises.ts | 28 +++++++++++++++++-- tests/corpus/7035-promise-all-tuple-turns.ts | 13 +++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index c30590619..3aecf87e3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -72,7 +72,28 @@ function lowerTuplePromiseAll( const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; const entriesT = arrayOf(voidPromise); const dynPromise: IrType = { kind: "promise", inner: DYN }; - const entries = elems.map((e): IrExpr => { + // A checked-dynamic entry's PromiseResolve can run a `then` getter, which + // Node runs after the whole array literal evaluated. With such an entry, + // every entry evaluates into a hidden local first. + const staged: IrStmt[] = []; + const values = !elems.some((e) => e.type.kind === "dyn") + ? elems + : elems.map((e): IrExpr => { + if (e.kind === "unitLit") return e; + if (isUnitType(e.type)) { + staged.push({ kind: "exprStmt", expr: e, loc }); + return { + kind: "unitLit", + unit: e.type.kind === "nullT" ? "null" : "undefined", + type: e.type, + loc, + }; + } + const local = lowerer.declareHiddenLocal("%allValue", e.type); + staged.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }); + const entries = values.map((e): IrExpr => { let promise: IrExpr; if (e.type.kind === "promise") promise = e; else if (e.type.kind === "dyn") @@ -164,7 +185,7 @@ function lowerTuplePromiseAll( loc, }); } - return { + const all: IrExpr = { kind: "libCall", fn: "promise.allTuple", args: [ @@ -174,6 +195,9 @@ function lowerTuplePromiseAll( type: resultT, loc, }; + return staged.length === 0 + ? all + : { kind: "seqExpr", stmts: staged, result: all, type: resultT, loc }; } /** `Promise.race([...])` on THE Promise global: the entries lower diff --git a/tests/corpus/7035-promise-all-tuple-turns.ts b/tests/corpus/7035-promise-all-tuple-turns.ts index 6989df9b9..1f8a8803d 100644 --- a/tests/corpus/7035-promise-all-tuple-turns.ts +++ b/tests/corpus/7035-promise-all-tuple-turns.ts @@ -26,6 +26,16 @@ async function text(): Promise { async function both(): Promise<[number, string]> { return Promise.all([num(), text()]); } +const thenable: any = { + get then() { + log.push("then getter"); + return (resolve: (v: number) => void) => resolve(1); + }, +}; +function evaluated(): Promise { + log.push("second entry evaluated"); + return Promise.resolve("s"); +} interface Item { id: number; } @@ -49,6 +59,9 @@ async function main(): Promise { log.push(`returned ${h}${i}`); const mixed = await Promise.all([later("x", 3, "x"), 5, later("y", 1, true), undefined]); log.push(`mixed ${mixed[0]} ${mixed[1]} ${mixed[2]} ${mixed[3]}`); + // A dynamic entry's `then` getter runs after the whole array evaluated. + const [fromThenable, second] = await Promise.all([thenable, evaluated()]); + log.push(`thenable ${fromThenable} ${second}`); const raw: any = JSON.parse('{"value":41}'); const [dynamic, counted] = await Promise.all([raw.value, later("z", 2, 2)]); log.push(`dynamic ${dynamic + counted}`); From 9e1027e13a35c1e6503f4e1b72e9c1aa3bb1af69 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:46:44 -0500 Subject: [PATCH 06/45] Run async generator steps in the AsyncLocalStorage context of their resumer An async generator kept the context of its creation for its whole life, and a store its body entered carried into every later step. Node runs each step in the execution that resumes it: a step starts with the context of the next(), return() or throw() call, a store it enters before its first await is the caller's after the call, and after an await the step continues in its own copy. Async generators now start each new step from the resumer's context and hand the prefix's context back at the step's first suspension, as synchronous generator steps and async function prefixes already do; the continuation after an await keeps the generator's own. Two requests that start from the settlement job, which runs from the loop, take Node's contexts too: a request queued behind a running step starts in that step's context, and a return() at a yield, whose unwind waits a turn, runs in the return() call's context. Corpus 7381 and 7382. --- .../7381-als-async-generator-steps.ts.json | 6 + .../7382-als-async-generator-steps.mjs.json | 6 + packages/runtime/src/scr_async.c | 80 +++++++++-- .../corpus/7381-als-async-generator-steps.ts | 133 ++++++++++++++++++ .../corpus/7382-als-async-generator-steps.mjs | 43 ++++++ .../7381-als-async-generator-steps.ts.json | 1 + .../7382-als-async-generator-steps.mjs.json | 1 + 7 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json create mode 100644 tests/corpus/7381-als-async-generator-steps.ts create mode 100644 tests/corpus/7382-als-async-generator-steps.mjs create mode 100644 tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json create mode 100644 tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json new file mode 100644 index 000000000..47f93314d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-als-async-generator-steps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json new file mode 100644 index 000000000..5f55ba7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-als-async-generator-steps.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 90e7dd04f..e6184ed9b 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -715,11 +715,13 @@ static void scr_als_prefix_end(ScrFiber *f) { } } -/* A synchronous generator has no context of its own: each step runs in its - * resumer's execution, as Node runs it. A fiber-backed one switches to its - * own slot, so its step starts from the resumer's store and a store it - * enters is the resumer's after the step (scr_als_prefix_end on the way - * back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */ +/* A generator step runs in its resumer's execution, as Node runs it. A + * fiber-backed generator switches to its own slot, so its step starts from + * the resumer's store and a store it enters is the resumer's after the + * step (scr_als_prefix_end on the way back); a stackless synchronous one + * runs in the resumer's slot (scr_gen_coro_hop). An async generator's step + * starts the same way, and after an await it continues in its own copy of + * the context, as an async function's body does after its prefix. */ static void scr_als_step_begin(ScrFiber *f) { ScrAlsCtx **slot = SCR_ALS_SLOT(); if (f->als != *slot) { @@ -5303,6 +5305,10 @@ struct ScrGen { * since that return takes its own turn to await x. */ bool return_queued; bool return_awaited; + /* The AsyncLocalStorage context of a return() request deferred at a + * yield (+1): the body's unwind runs in it, as Node resumes it from the + * return value's await. */ + ScrAlsCtx *return_ctx; #ifdef SCR_WORKERS bool context_abandoned; #endif @@ -5406,6 +5412,7 @@ void scr_gen_release(ScrGen *g) { scr_gen_slot_reset(&g->in); scr_gen_slot_reset(&g->ret); scr_gen_exc_reset(&g->async_error); + scr_als_ctx_release(g->return_ctx); if (g->fiber != NULL) { if (g->state == SCR_GEN_UNSTARTED) { /* Never ran: nothing on the stack owns anything — clean teardown. @@ -5760,6 +5767,12 @@ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen * if (release) scr_gen_release(g); } +/* Out of line, so the hop that synchronous generators share stays as it + * was: an async generator's step begins from its resumer's context and + * hands the prefix's context back at its first suspension. */ +static __attribute__((noinline)) void scr_async_gen_step_begin(ScrFiber *f) { scr_als_step_begin(f); } +static __attribute__((noinline)) void scr_async_gen_step_end(ScrFiber *f) { scr_als_prefix_end(f); } + #ifndef __wasi__ /* The stackless form of the hop: the coroutine frame runs on this stack * until its next yield or its final suspension returns here. `retain` @@ -5768,14 +5781,17 @@ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen * __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bool retain) { ScrFiber *f = g->fiber; if (retain) scr_gen_retain(g); + /* A synchronous generator has no AsyncLocalStorage context of its own: + * its step runs in the resumer's slot, as Node runs it in the execution + * that called next(). An async generator's step runs in its own slot, + * which a new step (not the continuation after an await) starts from the + * resumer's context (scr_als_step_begin). */ + if (g->is_async && g->state != SCR_GEN_AWAITING) scr_async_gen_step_begin(f); g->state = SCR_GEN_RUNNING; f->resumer = scr_current; ScrAlsCtx **resumer_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); - /* A synchronous generator has no AsyncLocalStorage context of its own: - * its step runs in the resumer's slot, as Node runs it in the execution - * that called next(). An async generator's step runs in its own. */ if (g->is_async) scr_als_active = &f->als; if (f->coro == NULL) f->entry(f, f->argpack); else scr_coro_resume(f->coro); @@ -5786,6 +5802,7 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); scr_als_active = resumer_slot; + if (g->is_async) scr_async_gen_step_end(f); scr_gen_switched_back(g, f, retain, true); } @@ -5806,8 +5823,10 @@ static void scr_gen_switch_in(ScrGen *g) { } #endif scr_gen_retain(g); + /* Each step starts from the resumer's context; an async generator's + * continuation after an await keeps its own. */ + if (!g->is_async || g->state != SCR_GEN_AWAITING) scr_als_step_begin(f); g->state = SCR_GEN_RUNNING; - if (!g->is_async) scr_als_step_begin(f); #ifdef _WIN32 ScrCtx here = scr_win_self(); #elif defined(__wasi__) @@ -5979,6 +5998,25 @@ void scr_gen_resume_throw(ScrGen *g) { static void scr_async_gen_start_queued(ScrGen *g); +/* Runs fn(g) with `ctx` (borrowed) as the active AsyncLocalStorage + * context. A settlement job runs from the loop: a request queued behind + * another starts in the context of the step before it (Node drains the + * queue in the generator's own execution), and a deferred return() in its + * call's. */ +static __attribute__((noinline)) void scr_async_gen_in_context(ScrGen *g, ScrAlsCtx *ctx, + void (*fn)(ScrGen *)) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_als_ctx_retain(ctx); + fn(g); + scr_als_restore(prev); +} + +/* Starts the queued requests in the context of the step before them. */ +static __attribute__((noinline)) void scr_async_gen_start_queued_in_own(ScrGen *g) { + scr_async_gen_in_context(g, g->fiber != NULL ? g->fiber->als : NULL, scr_async_gen_start_queued); +} + static void scr_async_gen_schedule_settle(ScrGen *g) { if (g->settle_queued) return; ScrFiber *job = scr_mem_calloc(sizeof *job); @@ -6026,6 +6064,11 @@ static bool scr_async_gen_settles_now(ScrGen *g) { return false; } +static __attribute__((noinline)) void scr_async_gen_capture_return_ctx(ScrGen *g) { + scr_als_ctx_release(g->return_ctx); + g->return_ctx = scr_als_ctx_retain(*SCR_ALS_SLOT()); +} + static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { const bool was_done = g->state == SCR_GEN_DONE; if (req->mode == SCR_AG_NEXT) { @@ -6041,6 +6084,7 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { /* At a yield, AsyncGeneratorYield awaits the return value before * the body resumes, so finally blocks run one turn later. */ g->return_queued = true; + scr_async_gen_capture_return_ctx(g); scr_async_gen_schedule_settle(g); return; } @@ -6071,6 +6115,16 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { } } +static void scr_async_gen_resume_returning(ScrGen *g); + +/* The deferred return() resumes in its call's context. */ +static __attribute__((noinline)) void scr_async_gen_resume_returning_in_context(ScrGen *g) { + ScrAlsCtx *ctx = g->return_ctx; + g->return_ctx = NULL; + scr_async_gen_in_context(g, ctx, scr_async_gen_resume_returning); + scr_als_ctx_release(ctx); +} + /* The return() request's await has taken its turn: resume the body with * the return completion. */ static void scr_async_gen_resume_returning(ScrGen *g) { @@ -6203,12 +6257,12 @@ static void scr_async_gen_job(ScrGen *g) { } if (g->return_queued) { scr_gen_retain(g); - scr_async_gen_resume_returning(g); + scr_async_gen_resume_returning_in_context(g); scr_gen_release(g); return; } scr_async_gen_finish_active(g, false); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } static void scr_async_gen_resume_ready(ScrGen *g) { @@ -6222,11 +6276,11 @@ static void scr_async_gen_resume_ready(ScrGen *g) { scr_gen_switch_in(g); if (g->async_error.kind != SCR_EXC_NONE) { scr_async_gen_finish_active(g, true); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } else if (g->state != SCR_GEN_AWAITING) { if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } else { scr_async_gen_schedule_settle(g); } diff --git a/tests/corpus/7381-als-async-generator-steps.ts b/tests/corpus/7381-als-async-generator-steps.ts new file mode 100644 index 000000000..4f6ace58d --- /dev/null +++ b/tests/corpus/7381-als-async-generator-steps.ts @@ -0,0 +1,133 @@ +// AsyncLocalStorage contexts of async generator steps. Each step starts in +// the context of the next(), return() or throw() call that resumes it, +// whatever context created the generator; a store the step enters before +// its first await is the caller's after the call, and after an await the +// step keeps its own copy. A request queued behind a running step starts +// in that step's context, and a return() at a yield unwinds in the +// return() call's context. Covers yield*, for await with break, and two +// generators resumed in turn. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; + +async function* steps(): AsyncGenerator { + log("body start"); + yield 1; + log("after the first yield"); + await null; + log("after an await"); + store.enterWith("inside"); + yield 2; + log("after the second yield"); + yield 3; + log("body end"); +} +const it = store.run("create", () => steps()); +await store.run("next-1", () => it.next()); +log("caller after next-1"); +await store.run("next-2", () => it.next()); +log("caller after next-2"); +await store.run("next-3", () => it.next()); +await store.run("next-4", () => it.next()); + +async function* prefixes(): AsyncGenerator { + store.enterWith("prefix"); + yield 1; + store.enterWith("step-2"); + await null; + log("after an await in step 2"); + yield 2; +} +await store.run("outer", async () => { + const gen = prefixes(); + const first = gen.next(); + log("synchronously after next"); + await first; + log("after awaiting next"); + const second = gen.next(); + log("synchronously after the second next"); + await second; + log("after awaiting the second next"); +}); + +async function* queued(): AsyncGenerator { + log("queued start"); + yield 1; + log("queued resumed"); + yield 2; +} +const q = queued(); +await Promise.all([store.run("q-1", () => q.next()), store.run("q-2", () => q.next())]); + +async function* inner(): AsyncGenerator { + log("inner start"); + yield "a"; + log("inner resumed"); + yield "b"; +} +async function* outerGen(): AsyncGenerator { + log("outer start"); + yield* inner(); + log("outer after yield*"); +} +const delegating = outerGen(); +await store.run("o-1", () => delegating.next()); +await store.run("o-2", () => delegating.next()); +await store.run("o-3", () => delegating.next()); + +async function* catching(): AsyncGenerator { + try { + yield 1; + } catch (e) { + log(`caught ${(e as Error).message}`); + yield 2; + } +} +const c = catching(); +await store.run("t-1", () => c.next()); +await store.run("t-2", () => c.throw(new Error("boom"))); + +async function* finallyGen(): AsyncGenerator { + try { + yield 1; + } finally { + log("finally"); + } +} +const f = finallyGen(); +await store.run("n", () => f.next()); +await store.run("return-call", () => f.return()); + +async function* endless(): AsyncGenerator { + let i = 0; + try { + while (true) { + log(`endless ${i}`); + yield i++; + } + } finally { + log("endless finally"); + } +} +await store.run("loop", async () => { + for await (const v of endless()) { + if (v === 1) break; + } +}); + +async function* named(name: string): AsyncGenerator { + log(`${name} 1`); + yield 1; + log(`${name} 2`); + yield 2; +} +const a = named("A"); +const b = named("B"); +await Promise.all([store.run("a-1", () => a.next()), store.run("b-1", () => b.next())]); +await Promise.all([store.run("b-2", () => b.next()), store.run("a-2", () => a.next())]); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7382-als-async-generator-steps.mjs b/tests/corpus/7382-als-async-generator-steps.mjs new file mode 100644 index 000000000..bbabdb592 --- /dev/null +++ b/tests/corpus/7382-als-async-generator-steps.mjs @@ -0,0 +1,43 @@ +// AsyncLocalStorage contexts of async generator steps from JavaScript: +// steps start in the context of the call that resumes them, a store a step +// enters before its first await is the caller's, queued requests start in +// the running step's context, and return() at a yield unwinds in the +// return() call's context. +import { AsyncLocalStorage } from "node:async_hooks"; +const s = new AsyncLocalStorage(); +const lines = []; +const g = (t) => lines.push(`${t}: ${s.getStore() ?? "none"}`); +async function* gen() { + g("body start"); + yield 1; + g("after first yield"); + await null; + g("after await"); + s.enterWith("inside"); + yield 2; + g("after second yield"); + yield 3; + g("body end"); +} +const it = s.run("create", () => gen()); +await s.run("next1", () => it.next()); +g("caller after next1"); +await s.run("next2", () => it.next()); +g("caller after next2"); +await s.run("next3", () => it.next()); +await s.run("next4", () => it.next()); +// concurrent next calls queued +async function* gen2() { g("g2 start"); yield 1; g("g2 resumed"); yield 2; } +const it2 = gen2(); +const p1 = s.run("q1", () => it2.next()); +const p2 = s.run("q2", () => it2.next()); +await Promise.all([p1, p2]); +// for await +async function* gen3() { for (let i = 0; i < 2; i++) { g("g3 step " + i); yield i; } } +await s.run("for-await", async () => { for await (const v of gen3()) g("loop body " + v); }); +// return/throw +async function* gen4() { try { yield 1; } finally { g("g4 finally"); } } +const it4 = gen4(); +await s.run("n", () => it4.next()); +await s.run("ret", () => it4.return()); +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json b/tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } diff --git a/tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json b/tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } From 14098794d08c09b7bbaf75566de7b0073a39b7df Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 15:46:43 -0500 Subject: [PATCH 07/45] Type the scenario promises of corpus 7119 and 7380 with JSDoc tsgo rejects a call of `resolve()` without an argument when the `new Promise()` that produced it has no type, and asks for a JSDoc hint; tsc-ts accepts it. The two programs compiled under tsc-ts only, so the checker-identity check failed on them. Each scenario function now declares `@returns {Promise}`, as corpus 1594 does. Their output is unchanged. --- tests/corpus/7119-als-net-event-context.mjs | 3 +++ tests/corpus/7380-als-http-event-context.mjs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/tests/corpus/7119-als-net-event-context.mjs b/tests/corpus/7119-als-net-event-context.mjs index 3fb95ffa8..d17d1c31c 100644 --- a/tests/corpus/7119-als-net-event-context.mjs +++ b/tests/corpus/7119-als-net-event-context.mjs @@ -15,6 +15,7 @@ const store = new AsyncLocalStorage(); const lines = []; const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +/** @returns {Promise} */ function echoScenario() { return new Promise((resolve) => { const server = store.run("create", () => @@ -61,6 +62,7 @@ function echoScenario() { }); } +/** @returns {Promise} */ function destroyScenario() { return new Promise((resolve) => { const server = net.createServer((sock) => sock.on("data", () => {})); @@ -78,6 +80,7 @@ function destroyScenario() { }); } +/** @returns {Promise} */ function closeScenarios() { return new Promise((resolve) => { let pending = 3; diff --git a/tests/corpus/7380-als-http-event-context.mjs b/tests/corpus/7380-als-http-event-context.mjs index a881cbe7e..fd6ef6225 100644 --- a/tests/corpus/7380-als-http-event-context.mjs +++ b/tests/corpus/7380-als-http-event-context.mjs @@ -17,6 +17,7 @@ const store = new AsyncLocalStorage(); const lines = []; const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +/** @returns {Promise} */ function httpScenario() { return new Promise((resolve) => { const server = store.run("create", () => @@ -56,6 +57,7 @@ function httpScenario() { }); } +/** @returns {Promise} */ function keepAliveScenario() { return new Promise((resolve) => { const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); @@ -88,6 +90,7 @@ const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); +/** @returns {Promise} */ function httpsScenario() { return new Promise((resolve) => { const server = https.createServer({ key, cert }, (req, res) => { @@ -116,6 +119,7 @@ function httpsScenario() { }); } +/** @returns {Promise} */ function tlsScenario() { return new Promise((resolve) => { const server = tls.createServer({ key, cert }, (sock) => { From 1d8d19e634982f238ccbfd204f4d7355d2a69efa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:05:08 -0500 Subject: [PATCH 08/45] Run stream events and callbacks in Node's AsyncLocalStorage contexts Stream events and callbacks ran with no store. Node's streams run each step in the context of the code that triggers it: a step deferred with process.nextTick (resume, emitReadable, endReadable, afterWrite, the destroy and finish steps) in the context that scheduled it, and other events in the context of the push(), write(), end(), destroy() or pipe() call that emits them. Native-backed streams run their events in the context of their handle. The runtime now captures the active context where it defers a stream step and enters it around the step's dispatch: - the stream tick queue keeps each tick's scheduling context; - the runtime's own process.nextTick jobs (zlib completions, pipeline and finished steps, readline) run in the context they were queued in, as user ticks already did; - fs read and write stream requests keep the context of the call that started them, so a stream's open, reads, writes and close follow its creation; - zlib streams keep their creation context and run each engine step's completion in it. In programs that use AsyncLocalStorage, stream.finished and stream.pipeline callbacks are bound to the context of their call, like the other completion callbacks. Programs that do not use AsyncLocalStorage compile to the same IR. child_process and http2 events are unchanged. Corpus 7383 and 7384. --- .../builtin-apis/asynclocalstorage.mdx | 5 +- .../generated/node-v24-internal.json | 34 ++- internal/compatibility/static-support.json | 2 +- .../frontend/lowering/completion-callbacks.ts | 13 +- .../corpus/7383-als-stream-events.ts.json | 6 + .../corpus/7384-als-stream-events.mjs.json | 6 + packages/runtime/src/scr_async.c | 8 +- packages/runtime/src/scr_fs_stream.c | 13 +- packages/runtime/src/scr_stream.c | 39 ++-- packages/runtime/src/scr_zlib_stream.c | 7 + tests/corpus/7383-als-stream-events.ts | 204 +++++++++++++++++ tests/corpus/7384-als-stream-events.mjs | 214 ++++++++++++++++++ .../7383-als-stream-events.ts.json | 1 + .../7384-als-stream-events.mjs.json | 1 + 14 files changed, 525 insertions(+), 28 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json create mode 100644 tests/corpus/7383-als-stream-events.ts create mode 100644 tests/corpus/7384-als-stream-events.mjs create mode 100644 tests/harness/corpus-costs/7383-als-stream-events.ts.json create mode 100644 tests/harness/corpus-costs/7384-als-stream-events.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 98fc92ea1..016b712f6 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -3,5 +3,8 @@ Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with. A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call. Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call. - Events and callbacks of other streams run with no store, where Node runs them in the context of the code that triggers them. + Stream events and callbacks run with the stores of the code that triggers them, as in Node. + Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call. + `fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them. + Events of `child_process` children and of `http2` sessions and streams do not yet run in the contexts Node gives them. `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 6046daf0e..b5542944d 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1392,7 +1392,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "chapter" @@ -1447,7 +1449,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1525,7 +1529,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1557,7 +1563,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1589,7 +1597,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1621,7 +1631,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1653,7 +1665,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1962,6 +1976,8 @@ "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2529,7 +2545,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index c57d6013e..640345a02 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts index 402741da6..a00e4924d 100644 --- a/packages/compiler/src/frontend/lowering/completion-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -1,7 +1,8 @@ /* Completion callbacks in programs that use AsyncLocalStorage. Node runs * the callback of an operation a callback-style API starts (fs.readFile, * crypto.randomBytes, zlib.gzip, dns.lookup, server.listen, net.connect, - * http.get, ...) with the stores active when the call started it. The runtime invokes these callbacks from + * http.get, stream.finished, stream.pipeline, ...) with the stores active + * when the call started it. The runtime invokes these callbacks from * the event loop, where the context is empty, so in programs whose * scheduling carries contexts (RuntimeFeatures asyncContext) each such * argument is bound to the active context at the call: als.bindCallback @@ -50,6 +51,16 @@ const COMPLETION_CALLBACKS: ReadonlyMap = new Map< ["net.listenPathCb", 2], ["process.stderrWriteBytesCb", 2], ["process.stdoutWriteBytesCb", 2], + // stream.finished and stream.pipeline callbacks, which Node binds to + // the context of the call (eos' and pipeline's AsyncResource.bind). + ["stream.finished", 1], + ["stream.finishedDyn", 1], + ["stream.finishedOpts", "last"], + ["stream.finishedOptsSignal", "last"], + ["stream.pipeline", "last"], + ["stream.pipelineDyn", "last"], + ["stream.pipelineStages", "last"], + ["stream.pipelineStagesDyn", "last"], ["zlib.cbOpts", 3], ["zlib.cbOptsDyn", 3], ["zlib.deflateCb", 1], diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json new file mode 100644 index 000000000..d59808c2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-als-stream-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json new file mode 100644 index 000000000..51171c8d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7384-als-stream-events.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index e6184ed9b..2bfffa59d 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1235,10 +1235,14 @@ static SCR_TL ScrNtick *scr_nt_tail = NULL; * it, because 'exit' listeners run AFTER the loop's teardown and may * enqueue ticks that must never run (Node) yet must not leak. */ +static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/); + +/* A tick runs with the AsyncLocalStorage context active when it was + * queued, as Node's process.nextTick does. */ void scr_next_tick(ScrClosure *cb /*moves*/) { ScrNtick *t = calloc(1, sizeof *t); if (!t) scr_oom(); - t->cb = cb; + t->cb = scr_als_bind(cb); if (scr_nt_tail) scr_nt_tail->next = t; else scr_nt_head = t; scr_nt_tail = t; @@ -4022,7 +4026,7 @@ double scr_set_timeout_handle_als(ScrClosure *cb, double ms) { return scr_set_timeout_handle(scr_als_bind(cb), ms); } double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_bind(cb)); } -void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } +void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(cb); } /* scr_next_tick binds */ void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } /* A typed callback bound to the context active when it was bound: the diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 70a5ab075..940068a7b 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -79,6 +79,9 @@ typedef struct ScrFssOp { int64_t result; /* answer or -errno */ int retries; /* WRITE: zero-byte retries */ ScrError *carry; /* FSYNC/CLOSE: the error to pass on (+1) */ + /* The AsyncLocalStorage context the step was started in (+1): its + * continuation runs in it, as Node runs an fs request's callback. */ + ScrAlsCtx *ctx; } ScrFssOp; static void scr_fss_work(void *p) { @@ -101,6 +104,7 @@ static void scr_fss_op_destroy(void *p) { free(op->buf); if (op->chunk) scr_bytes_release(op->chunk); if (op->carry) scr_error_release(op->carry); + scr_als_ctx_release(op->ctx); free(op); } @@ -111,13 +115,20 @@ static ScrFssOp *scr_fss_op_new(ScrStream *s, ScrFssOpKind kind, int fd) { op->kind = kind; op->fd = fd; op->pos = -1; + op->ctx = scr_als_capture(); return op; } static void scr_fss_after(void *p); +static void scr_fss_after_in_context(void *p) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrFssOp *)p)->ctx); + scr_fss_after(p); + scr_als_dispatch_leave(prev); +} + static void scr_fss_submit(ScrFssOp *op) { - scr_work_submit(op, &scr_fss_work, &scr_fss_after, &scr_fss_op_destroy); + scr_work_submit(op, &scr_fss_work, &scr_fss_after_in_context, &scr_fss_op_destroy); } /* ── close (Node's close/_close) ──────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index be5ccb910..835b1db9a 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -515,6 +515,10 @@ typedef struct ScrStreamTick { ScrError *err; /* owned or NULL (SCR_ST_ERROR) */ ScrClosure *cb; /* owned or NULL (SCR_ST_AFTER_WRITE / SCR_ST_WCB_ERR / SCR_ST_WRITE_ERR) */ bool hold; /* SCR_ST_AFTER_WRITE: counted in w.after_holds */ + /* The AsyncLocalStorage context active when the tick was scheduled (+1, + * NULL outside a store): Node schedules these steps with + * process.nextTick, so their events and callbacks run in it. */ + ScrAlsCtx *ctx; struct ScrStreamTick *next; } ScrStreamTick; @@ -531,6 +535,7 @@ static void scr_st_tick(ScrStream *s, ScrStreamTickOp op, ScrError *err /*moves* t->op = op; t->err = err; t->cb = cb; + t->ctx = scr_als_capture(); if (scr_st_tail) scr_st_tail->next = t; else scr_st_head = t; scr_st_tail = t; @@ -6861,6 +6866,21 @@ static void scr_stream_run_tick(ScrStreamTick *t) { static void scr_stream_dispatch(void); +/* A tick runs in the context it was scheduled in. */ +static void scr_stream_run_tick_in_context(ScrStreamTick *t) { + ScrAlsCtx *prev = scr_als_dispatch_enter(t->ctx); + scr_stream_run_tick(t); + scr_als_dispatch_leave(prev); +} + +static void scr_stream_tick_free(ScrStreamTick *t) { + scr_stream_release(t->s); + if (t->err) scr_error_release(t->err); + if (t->cb) scr_closure_release(t->cb); + scr_als_ctx_release(t->ctx); + free(t); +} + /* One tick-marker's dispatch: the queue's FIFO head (markers and entries * are enqueued 1:1 in the same order). After an uncaught throw the * remaining entries drop through the station's exc branch below, keeping @@ -6870,11 +6890,8 @@ static void scr_stream_dispatch_one(void) { if (t == NULL || scr_exc_pending()) return; scr_st_head = t->next; if (scr_st_head == NULL) scr_st_tail = NULL; - scr_stream_run_tick(t); - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_run_tick_in_context(t); + scr_stream_tick_free(t); if (scr_exc_pending()) scr_stream_dispatch(); /* its exc branch drops the rest */ } @@ -6883,11 +6900,8 @@ static void scr_stream_dispatch(void) { ScrStreamTick *t = scr_st_head; scr_st_head = t->next; if (scr_st_head == NULL) scr_st_tail = NULL; - scr_stream_run_tick(t); - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_run_tick_in_context(t); + scr_stream_tick_free(t); } /* An uncaught throw ends the process (the loop returns to main): the * undelivered ticks are DROPPED — release their references here so the @@ -6896,10 +6910,7 @@ static void scr_stream_dispatch(void) { while (scr_st_head != NULL) { ScrStreamTick *t = scr_st_head; scr_st_head = t->next; - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_tick_free(t); } scr_st_tail = NULL; } diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 54ffacf02..f1437fc2c 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -51,6 +51,9 @@ typedef struct ScrZlibStream { bool done_pending; /* Transform's kCallback: completion waits for _read */ double rlen_before; /* readableLength when the chunk's transform began */ ScrBytes *flush_bufs[6]; /* zero-length sentinels, one per flush kind */ + /* The AsyncLocalStorage context of the stream's creation (+1, NULL + * outside a store): Node runs the handle's step callbacks in it. */ + ScrAlsCtx *als; } ScrZlibStream; static void scr_zlib_stream_oom(void) { @@ -81,6 +84,7 @@ static void scr_zs_release_v(void *p) { for (size_t i = 0; i < 6; i++) { if (zs->flush_bufs[i]) scr_bytes_release(zs->flush_bufs[i]); } + scr_als_ctx_release(zs->als); free(zs); } @@ -242,6 +246,7 @@ static void scr_zs_step_after(void *payload) { ScrZlibStep *op = payload; ScrZlibStream *zs = op->zs; ScrStream *s = op->s; + ScrAlsCtx *prev = scr_als_dispatch_enter(zs->als); zs->write_in_progress = false; ScrZlibError ze; if (scr_zlib_engine_error(zs->eng, &ze)) { @@ -252,6 +257,7 @@ static void scr_zs_step_after(void *payload) { scr_zs_process(zs, s); } if (zs->pending_close) scr_zs_close_engine(zs); + scr_als_dispatch_leave(prev); } static void scr_zs_step_destroy(void *payload) { @@ -463,6 +469,7 @@ ScrStream *scr_zlib_stream_new(double mode_d, ScrDyn *opts) { ScrZlibStream *zs = calloc(1, sizeof *zs); if (!zs) scr_zlib_stream_oom(); zs->rc = 1; + zs->als = scr_als_capture(); zs->mode = mode; zs->eng = scr_zlib_engine_from_options(mode, &zo); scr_zlib_options_drop(&zo); diff --git a/tests/corpus/7383-als-stream-events.ts b/tests/corpus/7383-als-stream-events.ts new file mode 100644 index 000000000..077e15346 --- /dev/null +++ b/tests/corpus/7383-als-stream-events.ts @@ -0,0 +1,204 @@ +// AsyncLocalStorage contexts of stream events and callbacks. A stream +// step that Node defers with process.nextTick runs in the context of the +// code that scheduled it; other events run in the context of the call that +// emits them: push(), write(), end(), destroy() or pipe(). stream.finished +// and stream.pipeline callbacks run in the context of their call. fs read +// and write streams and zlib streams run their events in the context that +// created them, as Node's handles do. +import { AsyncLocalStorage } from "node:async_hooks"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { finished, pipeline, Readable, Transform, Writable } from "node:stream"; +import { pipeline as pipelinePromise } from "node:stream/promises"; +import * as zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; +const step = (fn: (done: () => void) => void): Promise => + new Promise((resolve) => fn(resolve)); + +// pipe(): the destination's writes and 'finish' follow the pipe() call. +await step((done) => { + const source = new Readable({ read(): void {} }); + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("pipe write"); + cb(); + }, + }); + sink.on("finish", () => { + log("pipe finish"); + done(); + }); + store.run("pipe", () => source.pipe(sink)); + store.run("push", () => { + source.push("a"); + source.push(null); + }); +}); + +// A Transform's steps follow write() and end(); 'end' follows end(). +await step((done) => { + const upper = new Transform({ + transform(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null, data?: string) => void): void { + log("transform"); + cb(null, chunk.toString().toUpperCase()); + }, + flush(cb: (error?: Error | null, data?: string) => void): void { + log("flush"); + cb(null, "!"); + }, + }); + upper.on("data", () => log("transform data")); + upper.on("end", () => { + log("transform end"); + done(); + }); + store.run("write", () => upper.write("a")); + store.run("end", () => upper.end()); +}); + +// A write that completes later: its callback, _final, 'prefinish', the +// end() callback, 'finish' and 'close' follow the write() call. +await step((done) => { + const slow = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("slow write"); + setImmediate(cb); + }, + final(cb: (error?: Error | null) => void): void { + log("final"); + cb(); + }, + }); + slow.on("prefinish", () => log("prefinish")); + slow.on("finish", () => log("finish")); + slow.on("close", () => { + log("close"); + done(); + }); + store.run("slow-write", () => slow.write("a", () => log("write callback"))); + store.run("slow-end", () => slow.end(() => log("end callback"))); +}); + +// 'readable' and 'end' follow the push() that triggers them. +await step((done) => { + const source = new Readable({ read(): void {} }); + source.on("readable", () => { + log("readable"); + while (source.read() !== null) {} + }); + source.on("end", () => { + log("readable end"); + done(); + }); + store.run("push-x", () => source.push("x")); + setTimeout(() => store.run("push-eof", () => source.push(null)), 5); +}); + +// destroy(): 'error' and 'close' follow the destroy() call. +await step((done) => { + const source = new Readable({ read(): void {} }); + source.on("error", () => log("destroy error")); + source.on("close", () => { + log("destroy close"); + done(); + }); + store.run("destroy", () => source.destroy(new Error("stopped"))); +}); + +// finished() and pipeline() callbacks follow their call, also when another +// context ends or destroys the stream. +await step((done) => { + const source = new Readable({ read(): void {} }); + store.run("finished", () => + finished(source, () => { + log("finished callback"); + done(); + }), + ); + source.resume(); + store.run("finished-eof", () => source.push(null)); +}); +await step((done) => { + const source = new Readable({ read(): void {} }); + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + cb(); + }, + }); + store.run("pipeline", () => + pipeline(source, sink, (err: NodeJS.ErrnoException | null) => { + log(`pipeline callback ${err ? err.message : "ok"}`); + done(); + }), + ); + setTimeout(() => store.run("pipeline-destroy", () => source.destroy(new Error("boom"))), 5); +}); + +// stream/promises pipeline: the stages follow the call; after the await +// the caller's context is back. +await store.run("promise-pipeline", () => + pipelinePromise( + Readable.from(["a"]), + new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("promise pipeline write"); + cb(); + }, + }), + ), +); +log("after the promise pipeline"); + +// fs streams: the events follow the context that created the stream. +const file = path.join(os.tmpdir(), `scriptc-7383-${process.pid}.txt`); +await step((done) => { + const out = store.run("fs-write-create", () => fs.createWriteStream(file)); + out.on("finish", () => log("fs finish")); + out.on("close", () => { + log("fs write close"); + done(); + }); + store.run("fs-write", () => out.write("hello", () => log("fs write callback"))); + store.run("fs-end", () => out.end()); +}); +await step((done) => { + const input = store.run("fs-read-create", () => fs.createReadStream(file)); + store.run("fs-listen", () => { + input.on("open", () => log("fs open")); + input.on("data", () => log("fs data")); + input.on("end", () => log("fs end")); + input.on("close", () => { + log("fs read close"); + done(); + }); + }); +}); +fs.unlinkSync(file); + +// zlib streams: the events follow the context that created the stream. +await step((done) => { + const gzip = store.run("gzip-create", () => zlib.createGzip()); + const gunzip = store.run("gunzip-create", () => zlib.createGunzip()); + const parts: Buffer[] = []; + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("gunzip output"); + parts.push(chunk); + cb(); + }, + }); + store.run("zlib-pipeline", () => + pipeline(Readable.from(["hello ", "world"]), gzip, gunzip, sink, () => { + log(`zlib pipeline callback ${Buffer.concat(parts).toString()}`); + done(); + }), + ); +}); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7384-als-stream-events.mjs b/tests/corpus/7384-als-stream-events.mjs new file mode 100644 index 000000000..6d67bf26f --- /dev/null +++ b/tests/corpus/7384-als-stream-events.mjs @@ -0,0 +1,214 @@ +// AsyncLocalStorage contexts of stream events and callbacks, from +// JavaScript. Stream steps that Node defers with process.nextTick run in +// the context that scheduled them: resume() after a 'data' listener, +// uncork() from a tick, a write after end(), a push() from a user tick. +// Other events follow the call that emits them. stream.finished callbacks +// run in the context of their call, with stream/promises' finished() +// resolving in the context that awaits it. readline lines follow the +// createInterface() call that resumes the input, and zlib streams run +// their events and flush() callbacks in the context that created them. +import { AsyncLocalStorage } from "node:async_hooks"; +import readline from "node:readline"; +import { Duplex, finished, PassThrough, Readable, Writable } from "node:stream"; +import { finished as finishedPromise } from "node:stream/promises"; +import zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +const step = (fn) => new Promise((resolve) => fn(resolve)); + +// pipe() and unpipe() events, and writes through a PassThrough. +await step((done) => { + const through = new PassThrough(); + const sink = new Writable({ write(chunk, encoding, cb) { log("pipe write"); cb(); } }); + sink.on("pipe", () => log("pipe event")); + sink.on("unpipe", () => log("unpipe event")); + store.run("pipe", () => through.pipe(sink)); + store.run("through-write", () => through.write("q")); + setTimeout(() => { + store.run("unpipe", () => through.unpipe(sink)); + done(); + }, 5); +}); + +// cork() and uncork() from a tick: the writes follow the tick's context. +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { log("corked write"); cb(); } }); + sink.on("finish", () => { + log("corked finish"); + done(); + }); + store.run("cork", () => { + sink.cork(); + sink.write("a"); + sink.write("b"); + }); + store.run("uncork", () => process.nextTick(() => sink.uncork())); + setTimeout(() => store.run("corked-end", () => sink.end()), 5); +}); + +// A write after end(): its callback and 'error' follow the write() call. +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { cb(); } }); + sink.on("error", () => log("write after end error")); + sink.end(); + store.run("late-write", () => + sink.write("x", () => { + log("late write callback"); + done(); + }), + ); +}); + +// A failing _write: the callback, 'error' and 'close' follow write(). +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { cb(new Error("bad")); } }); + sink.on("error", () => log("failed write error")); + sink.on("close", () => { + log("failed write close"); + done(); + }); + store.run("failed-write", () => sink.write("x", () => log("failed write callback"))); +}); + +// A Duplex that pushes from _write: 'data' and 'end' follow the resume() +// tick, which the 'data' listener scheduled with no store. +await step((done) => { + const duplex = new Duplex({ + read() {}, + write(chunk, encoding, cb) { + log("duplex write"); + this.push(chunk); + cb(); + }, + }); + duplex.on("data", () => { + log("duplex data"); + duplex.push(null); + }); + duplex.on("end", () => { + log("duplex end"); + done(); + }); + store.run("duplex-write", () => duplex.write("z")); +}); + +// A push() from a user tick, a 'data' listener registered in a store with +// setEncoding(), and PassThrough's 'end' and 'close' after end(). +await step((done) => { + const source = new Readable({ read() {} }); + source.on("data", () => log("tick data")); + source.on("end", () => { + log("tick end"); + done(); + }); + store.run("user-tick", () => + process.nextTick(() => { + log("user tick"); + source.push("x"); + }), + ); + setTimeout(() => store.run("tick-eof", () => source.push(null)), 5); +}); +await step((done) => { + const source = new Readable({ read() {} }); + source.setEncoding("utf8"); + store.run("data-listener", () => source.on("data", () => log("text data"))); + source.on("end", () => { + log("text end"); + done(); + }); + source.push("hi"); + source.push(null); +}); +await step((done) => { + const through = new PassThrough(); + through.on("end", () => log("through end")); + through.on("close", () => { + log("through close"); + done(); + }); + through.resume(); + store.run("through-end", () => through.end("last")); +}); + +// finished() on a Writable, and stream/promises' finished(). +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { setTimeout(cb, 1); } }); + store.run("finished-writable", () => + finished(sink, () => { + log("finished callback"); + done(); + }), + ); + sink.on("finish", () => log("writable finish")); + store.run("writable-end", () => { + sink.write("a"); + sink.end(); + }); +}); +await step((done) => { + const source = new Readable({ read() {} }); + source.resume(); + store.run("finished-promise", () => finishedPromise(source)).then(() => { + log("finished promise"); + done(); + }); + setTimeout(() => store.run("promise-eof", () => source.push(null)), 5); +}); + +// Readable.from() over an async generator: the reads follow the resume() +// tick, scheduled with no store. +await step((done) => { + async function* letters() { + log("generator start"); + yield "a"; + log("generator end"); + } + const source = store.run("from", () => Readable.from(letters())); + source.on("data", () => log("from data")); + source.on("end", () => { + log("from end"); + done(); + }); +}); + +// readline over a Readable: createInterface() resumes the input. +await step((done) => { + const input = Readable.from(["a\nb\n"]); + const rl = store.run("readline", () => readline.createInterface({ input })); + rl.on("line", (line) => log(`line ${line}`)); + rl.on("close", () => { + log("readline close"); + done(); + }); +}); + +// zlib streams: events and flush() callbacks follow the creation context. +await step((done) => { + const gzip = store.run("gzip-create", () => zlib.createGzip()); + store.run("gzip-listen", () => { + gzip.on("data", () => log("gzip data")); + gzip.on("end", () => { + log("gzip end"); + done(); + }); + }); + store.run("gzip-write", () => { + gzip.write("hello"); + gzip.end(); + }); +}); +await step((done) => { + const deflate = store.run("deflate-create", () => zlib.createDeflate()); + store.run("deflate-flush", () => deflate.flush(() => log("deflate flush callback"))); + deflate.on("data", () => {}); + deflate.on("end", () => { + log("deflate end"); + done(); + }); + store.run("deflate-end", () => deflate.end("x")); +}); + +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7383-als-stream-events.ts.json b/tests/harness/corpus-costs/7383-als-stream-events.ts.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7383-als-stream-events.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7384-als-stream-events.mjs.json b/tests/harness/corpus-costs/7384-als-stream-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7384-als-stream-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } From 8d5b3377aa54e2cbfbc89297b3559114cc903808 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:33:09 -0500 Subject: [PATCH 09/45] Represent Dates as objects with identity A Date is a refcounted object holding its time value, shared with the checked-dynamic Date handle, so it keeps its identity through unknown. Dates compile in arrays (includes, indexOf, lastIndexOf, join, default sort by toString() text), in Date | undefined and Date | null slots, as Map and Set keys and values, in index signatures, and in records, tuples, class fields, closures, promises and generators. === compares identity, relational operators compare time values, new Date(date) makes a new Date, and JSON.stringify writes toJSON(). Closes #225 Co-authored-by: Max B --- .../content/limitations/builtin-apis/date.mdx | 9 +- .../compiler/src/backend/llvm/debug-info.ts | 7 +- packages/compiler/src/backend/llvm/dyn.ts | 15 ++- .../src/backend/llvm/emit-function.ts | 12 +- .../compiler/src/backend/llvm/emitter.test.ts | 3 +- packages/compiler/src/backend/llvm/emitter.ts | 4 +- .../compiler/src/backend/llvm/expr-async.ts | 24 ++-- .../src/backend/llvm/expr-containers.ts | 16 +++ .../src/backend/llvm/expr-stream-bridges.ts | 7 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 35 +++++- packages/compiler/src/backend/llvm/publish.ts | 6 +- packages/compiler/src/backend/llvm/shapes.ts | 9 +- .../src/backend/llvm/stack-captures.ts | 8 +- packages/compiler/src/backend/llvm/walkers.ts | 15 +++ .../compiler/src/diagnostics/diagnostic.ts | 2 +- .../src/frontend/lowering/builtins/dates.ts | 8 +- .../src/frontend/lowering/call-signatures.ts | 1 - .../lowering/containers/array-callbacks.ts | 13 +- .../lowering/containers/array-methods.ts | 17 ++- .../frontend/lowering/expressions/binary.ts | 45 ++++--- .../src/frontend/lowering/lower-classes.ts | 9 +- .../src/frontend/lowering/lower-generators.ts | 2 +- .../src/frontend/lowering/lower-stmts.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 15 +-- packages/compiler/src/ir/ir.test.ts | 3 +- packages/compiler/src/ir/ir.ts | 27 +++- packages/compiler/src/ir/validate.ts | 21 +++- .../compiler/test/collection-keys.test.ts | 4 +- packages/compiler/test/ir.test.ts | 11 +- .../tests/corpus/5965-date-objects.ts.json | 6 + .../diagnostics/date-union-fences.ts.json | 6 - packages/runtime/src/scr_array.c | 47 ++++++- packages/runtime/src/scr_array.h | 4 + packages/runtime/src/scr_dyn.h | 5 + packages/runtime/src/scr_json.h | 4 + packages/runtime/src/scr_lib.c | 110 +++++++++++++---- packages/runtime/src/scr_number.h | 17 +++ tests/corpus/5965-date-objects.ts | 115 ++++++++++++++++++ tests/diagnostics/component-fences.ts | 6 +- tests/diagnostics/date-union-fences.ts | 22 ---- tests/diagnostics/stdlib-fence.ts | 8 -- tests/diagnostics/utility-types.ts | 8 +- .../class-interface-refinements.ts.txt | 2 +- .../__snapshots__/component-fences.ts.txt | 30 ++--- .../__snapshots__/date-union-fences.ts.txt | 56 --------- tests/harness/__snapshots__/generics.ts.txt | 2 +- .../index-signature-values.ts.txt | 4 +- .../harness/__snapshots__/stdlib-fence.ts.txt | 101 ++++++--------- tests/harness/__snapshots__/tuples.ts.txt | 4 +- .../__snapshots__/utility-types.ts.txt | 17 +-- .../corpus-costs/5965-date-objects.ts.json | 1 + 51 files changed, 571 insertions(+), 354 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json create mode 100644 tests/corpus/5965-date-objects.ts delete mode 100644 tests/diagnostics/date-union-fences.ts delete mode 100644 tests/harness/__snapshots__/date-union-fences.ts.txt create mode 100644 tests/harness/corpus-costs/5965-date-objects.ts.json diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index ac8bf9115..89266b745 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1,5 +1,6 @@ -- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported. - Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. - JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. - A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. +- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported. + Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators. + Unions of a Date with other non-nullish types are unsupported. + Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. + A Date converted to `unknown` is the same Date object. See [Dates and timezones](#dates-and-timezones) for timezone data. diff --git a/packages/compiler/src/backend/llvm/debug-info.ts b/packages/compiler/src/backend/llvm/debug-info.ts index 4e37ee5bc..3a75c6263 100644 --- a/packages/compiler/src/backend/llvm/debug-info.ts +++ b/packages/compiler/src/backend/llvm/debug-info.ts @@ -139,7 +139,6 @@ export class LlvmDebugInfo { let id: string; switch (type.kind) { case "f64": - case "date": case "procStream": id = this.add( `!DIBasicType(name: ${quoted(type.kind === "f64" ? "number" : type.kind)}, size: 64, encoding: DW_ATE_float)`, @@ -190,7 +189,7 @@ export class LlvmDebugInfo { ? [] : [ this.add( - `!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" || arm.kind === "date" ? 64 : this.pointerBits})`, + `!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" ? 64 : this.pointerBits})`, ), ], ); @@ -239,9 +238,7 @@ export class LlvmDebugInfo { // Scalar forward captures use an array-backed TDZ box. Expose that // exceptional representation opaquely rather than reporting a bad value. const opaqueBox = - local.boxed && - local.tdz && - (local.type.kind === "f64" || local.type.kind === "bool" || local.type.kind === "date"); + local.boxed && local.tdz && (local.type.kind === "f64" || local.type.kind === "bool"); const type = opaqueBox ? this.pointer( this.add( diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..a46c81658 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -421,7 +421,7 @@ export class LlDyn { /** The value LLVM type of a dynCheck result / toDyn operand for `t`. */ private valTy(t: IrType): string { - return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" + return t.kind === "f64" || t.kind === "procStream" ? "double" : t.kind === "bool" ? "i1" @@ -1856,10 +1856,10 @@ export class LlDyn { }; switch (t.kind) { case "date": { - host.declare(`declare double @scr_dyn_native_date_value(ptr)`); + host.declare(`declare ptr @scr_dyn_native_date_check(ptr, ptr)`); const r = B.tmp(); - B.line(`${r} = call double @scr_dyn_native_date_value(ptr %d)`); - B.terminate(`ret double ${r}`); + B.line(`${r} = call ptr @scr_dyn_native_date_check(ptr %d, ptr %path)`); + B.terminate(`ret ptr ${r}`); break; } case "url": @@ -2645,11 +2645,10 @@ export class LlDyn { let sourceAccessor: { name: string; release: string } | null = null; switch (t.kind) { case "date": { - // A Date handle holding the same time value; native Dates are - // immutable values, and so is the handle. - host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`); + // A Date handle holding the same Date object. + host.declare(`declare ptr @scr_dyn_native_date(ptr)`); const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`); + B.line(`${r} = call ptr @scr_dyn_native_date(ptr %v)`); B.terminate(`ret ptr ${r}`); break; } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 16e654393..501224369 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -301,11 +301,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { // ScrGen owns the frame), so its OUT slot address is fixed. let out: string | null = null; const yieldKind = fn.generator?.yieldT.kind; - if ( - !emitter.wasi && - fn.async !== true && - (yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool") - ) { + if (!emitter.wasi && fn.async !== true && (yieldKind === "f64" || yieldKind === "bool")) { emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`); const gen = B.tmp(); B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`); @@ -701,7 +697,6 @@ export function emitGenScaffolding(emitter: LlEmitter): string[] { tr.push(` br label %done ; void body: the done value is undefined (NONE)`); break; case "f64": - case "date": emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`); break; @@ -1050,7 +1045,6 @@ export function emitAsyncAwaitCall( emitter.declare(`declare void @scr_await_void(ptr)`); return ` call void @scr_await_void(ptr ${promise})`; case "f64": - case "date": emitter.declare(`declare double @scr_await_f64(ptr)`); return ` ${dest} = call double @scr_await_f64(ptr ${promise})`; case "bool": @@ -1105,7 +1099,7 @@ export function emitAsyncAwaitCall( const kind = ret.kind === "void" ? "void" - : ret.kind === "f64" || ret.kind === "date" + : ret.kind === "f64" ? "f64" : ret.kind === "bool" ? "bool" @@ -1184,7 +1178,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { case "void": return; case "f64": - case "date": emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); emitter.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`); return; @@ -1209,7 +1202,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { emitter.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`); break; case "f64": - case "date": emitter.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); emitter.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`); break; diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index d1fd62a83..8653dfd4e 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -1,7 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, - DATE_T, F64, PROCSTREAM_T, STRING, @@ -287,7 +286,7 @@ test("shared exceptional exits preserve the function's scalar return ABI", () => }); test("exceptional exits use each scalar kind's LLVM return type", () => { - for (const type of [F64, DATE_T, BOOL, PROCSTREAM_T]) { + for (const type of [F64, BOOL, PROCSTREAM_T]) { for (const ownedLocals of [false, true]) { const module = moduleFor([call(), call(), { kind: "throw", value: literal("failed"), loc }]); const work = module.functions[1]!; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..8db63daa3 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1762,7 +1762,6 @@ export class LlEmitter { if (POINTER_KINDS.has(t.kind)) return "ptr"; switch (t.kind) { case "f64": - case "date": return "double"; case "bool": return "i1"; @@ -1895,7 +1894,6 @@ export class LlEmitter { this.declare(`declare void @scr_promise_fulfill_void(ptr)`); return [` call void @scr_promise_fulfill_void(ptr ${promise})`]; case "f64": - case "date": this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); return [` call void @scr_promise_fulfill_f64(ptr ${promise}, double %r)`]; case "bool": @@ -2125,7 +2123,7 @@ export class LlEmitter { return { name: "", type }; } const t = B.tmp(); - if (type.kind === "f64" || type.kind === "date") { + if (type.kind === "f64") { this.declare(`declare double @scr_coro_direct_f64(ptr)`); B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`); } else if (type.kind === "bool") { diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index b5d765b16..ee9ff2983 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -184,7 +184,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic } const v = host.emitExpr(e.args[0]!); const t = e.args[0]!.type; - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); B.line(`call void @scr_promise_fulfill_f64(ptr ${p}, double ${v.name})`); } else if (t.kind === "bool") { @@ -500,7 +500,7 @@ export function emitAsyncExpr( host.declare(`declare void @scr_async_gen_hop_done()`); B.line(`call void @scr_async_gen_hop_done()`); } - if (yt.kind === "f64" || yt.kind === "date") { + if (yt.kind === "f64") { if (host.currentCoro !== null) { const coro = host.currentCoro; if (coro.out !== null) host.emitGenSlotStore(coro.out, "f64", v.name); @@ -582,7 +582,7 @@ export function emitAsyncExpr( return { name: "", type: e.type }; } const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { host.declare(`declare double @scr_gen_take_in_f64()`); B.line(`${t} = call double @scr_gen_take_in_f64()`); return { name: t, type: e.type }; @@ -710,12 +710,12 @@ export function emitAsyncExpr( throw new InternalCompilerError(`llvm emitter bug: genResume ${e.mode} on async`); const gb = emitBorrowedInput(host, e.gen); if (e.mode === "value") { - if (!host.wasi && (e.type.kind === "f64" || e.type.kind === "date")) + if (!host.wasi && e.type.kind === "f64") return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "f64"), type: e.type }; if (!host.wasi && e.type.kind === "bool") return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "bool"), type: e.type }; const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { host.declare(`declare double @scr_gen_take_out_f64(ptr)`); B.line(`${t} = call double @scr_gen_take_out_f64(ptr ${gb.name})`); return { name: t, type: e.type }; @@ -752,7 +752,7 @@ export function emitAsyncExpr( store(a.name, e.arg!.type); }; const parkIn = (name: string, t: IrType): void => { - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_gen_in_f64(ptr, double)`); B.line(`call void @scr_gen_in_f64(ptr ${g.name}, double ${name})`); } else if (t.kind === "bool") { @@ -787,7 +787,7 @@ export function emitAsyncExpr( const a = host.emitExpr(e.arg); const t = e.arg.type; if (isRefCounted(t)) host.toHeap(a); - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`); call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`; } else if (t.kind === "bool") { @@ -806,7 +806,7 @@ export function emitAsyncExpr( const a = host.emitExpr(e.arg); const t = e.arg.type; if (isRefCounted(t)) host.toHeap(a); - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`); call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`; } else if (t.kind === "bool") { @@ -849,7 +849,7 @@ export function emitAsyncExpr( B.line(`call void @scr_gen_ret_none(ptr ${g.name})`); } else { sendArg((name, t) => { - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_gen_ret_f64(ptr, double)`); B.line(`call void @scr_gen_ret_f64(ptr ${g.name}, double ${name})`); } else if (t.kind === "bool") { @@ -960,7 +960,7 @@ export function emitAsyncExpr( return { name: "", type: e.type }; } const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { const f64Await = host.awaitResultFn("f64"); host.declare(`declare double @${f64Await}(ptr)`); B.line(`${t} = call double @${f64Await}(ptr ${pr.name})`); @@ -1050,7 +1050,7 @@ export function emitAsyncExpr( const peek = host.unionPeek(u.name, def.id); if (host.coroAwait) host.emitCoroAwait(peek); let awaited: LlValue; - if (inner.kind === "f64" || inner.kind === "date") { + if (inner.kind === "f64") { const f64Await = host.awaitResultFn("f64"); host.declare(`declare double @${f64Await}(ptr)`); const x = B.tmp(); @@ -1136,7 +1136,6 @@ export function emitAsyncExpr( let resolve: string; const kindNums: Partial> = { f64: 0, - date: 0, bool: 1, string: 2, void: 3, @@ -1191,7 +1190,6 @@ export function emitAsyncExpr( B.line(`${p} = call ptr @scr_promise_new()`); const kindNums: Partial> = { f64: 0, - date: 0, bool: 1, string: 2, void: 3, diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 2f4cd98e9..b70bfcd23 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -863,6 +863,12 @@ export function emitArrIntrinsic( B.line(`${t} = call ptr @${helper}(ptr ${r.name}, ptr ${sep.name})`); return host.own({ name: t, type: e.type }); } + if (elem.kind === "date") { + host.declare(`declare ptr @scr_date_arr_join(ptr, ptr)`); + const t = B.tmp(); + B.line(`${t} = call ptr @scr_date_arr_join(ptr ${r.name}, ptr ${sep.name})`); + return host.own({ name: t, type: e.type }); + } host.declare(`declare ptr @scr_arr_join(ptr, ptr)`); const t = B.tmp(); B.line(`${t} = call ptr @scr_arr_join(ptr ${r.name}, ptr ${sep.name})`); @@ -887,6 +893,16 @@ export function emitArrIntrinsic( case "sortPrimitive": case "toSortedPrimitive": { if (e.method === "sortPrimitive") host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (elem.kind === "date") { + // Dates order by their toString() text. + host.declare(`declare ptr @scr_arr_sort_by_text(ptr, i1 zeroext, ptr)`); + host.declare(`declare ptr @scr_date_object_to_string(ptr)`); + const sorted = B.tmp(); + B.line( + `${sorted} = call ptr @scr_arr_sort_by_text(ptr ${r.name}, i1 ${e.method === "toSortedPrimitive" ? 1 : 0}, ptr @scr_date_object_to_string)`, + ); + return host.own({ name: sorted, type: e.type }); + } host.declare(`declare ptr @scr_arr_sort_primitive(ptr, i1 zeroext)`); const result = B.tmp(); B.line( diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 7cb0fa772..877a01601 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -118,12 +118,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri if (inner.kind === "void") { host.declare(`declare void @scr_promise_fulfill_void(ptr)`); B.line(`call void @scr_promise_fulfill_void(ptr %dst)`); - } else if ( - inner.kind === "f64" || - inner.kind === "date" || - inner.kind === "procStream" || - inner.kind === "bool" - ) { + } else if (inner.kind === "f64" || inner.kind === "procStream" || inner.kind === "bool") { const fn = inner.kind !== "bool" ? "scr_promise_fulfill_f64" : "scr_promise_fulfill_bool"; host.declare( `declare void @${fn}(ptr, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""})`, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..753f296e6 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -9,7 +9,7 @@ import { emitTestLibCall } from "./lib-test.js"; import { InternalCompilerError } from "../../errors.js"; import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; -import type { IrExpr, IrType } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrType } from "../../ir/ir.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { emitJsLibCall } from "./jsval.js"; import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js"; @@ -569,6 +569,39 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // The deep comparisons' number test: computed here, with no runtime call. if (e.fn === "assert.sameValue") return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type }; + // The runtime's Date functions take and answer time values: a Date + // operand passes its time value, and a Date result is a new Date object. + const callArgs = args.map((a) => + a.type.kind === "date" ? { name: emitDateTime(host, a.name), type: F64 } : a, + ); + if (e.type.kind === "date") { + const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs); + host.declare(`declare ptr @scr_date_new(double)`); + const date = B.tmp(); + B.line(`${date} = call ptr @scr_date_new(double ${ms.name})`); + return host.own({ name: date, type: e.type }); + } + return emitSymbolLibCall(host, e, sym, callArgs); +} + +/** A Date object's time value (ScrDate.ms). */ +function emitDateTime(host: LlvmEmitterContext, date: string): string { + const B = host.B; + const field = B.tmp(); + const ms = B.tmp(); + B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`); + B.line(`${ms} = load double, ptr ${field}`); + return ms; +} + +/** The call of runtime function `sym` with evaluated arguments. */ +function emitSymbolLibCall( + host: LlvmEmitterContext, + e: LibCallExpr, + sym: string, + args: readonly LlValue[], +): LlValue { + const B = host.B; const argDecls = args.map((a) => { const ty = host.llType(a.type); return ty === "i1" ? "i1 zeroext" : ty; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 60270adda..62fdd75ec 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -596,10 +596,10 @@ export class PublishWalkers { } } -/** Unpublishable types stored as scalars (dates are time values, process - * streams file descriptors): no pointer to push, and always a value. */ +/** Unpublishable types stored as scalars (process streams are file + * descriptors): no pointer to push, and always a value. */ function scalarRefusal(t: IrType): boolean { - return t.kind === "date" || t.kind === "procStream"; + return t.kind === "procStream"; } /** The Node.js implementation's refusal for a value of type `t`. */ diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index d13f80a78..e6e375223 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1099,6 +1099,7 @@ export function arrNewCall( elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace elem.kind === "dyn" || // native collection seeds/drains: traced REF elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) + elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside) (elem.kind === "array" && traceAdapter(host, elem) !== null); if (!useRef) { if (small) { @@ -1162,7 +1163,6 @@ const SCR_ARR_OPS_RELEASE_NOTHING = 2; export function boxNewCall(host: ShapeHost, t: IrType): string { const plain: Partial> = { f64: 0, - date: 0, procStream: 0, bool: 1, string: 2, @@ -1187,11 +1187,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string { /** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */ export function boxAccess(t: IrType): "f64" | "bool" | "ref" { - return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" - ? "f64" - : t.kind === "bool" - ? "bool" - : "ref"; + return t.kind === "f64" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref"; } /* ── record shapes ────────────────────────────────────────────────────── */ @@ -1207,7 +1203,6 @@ export function llFieldType( if (POINTER_KINDS.has(t.kind)) return "ptr"; switch (t.kind) { case "f64": - case "date": case "procStream": return "double"; case "bool": diff --git a/packages/compiler/src/backend/llvm/stack-captures.ts b/packages/compiler/src/backend/llvm/stack-captures.ts index f0812b7b0..40e1a0428 100644 --- a/packages/compiler/src/backend/llvm/stack-captures.ts +++ b/packages/compiler/src/backend/llvm/stack-captures.ts @@ -105,13 +105,7 @@ export class StackCaptures { // sites, including duplicated finally bodies and versioned loops. if (!this.locals.has(id) || this.remaining < 48) return null; const ref = isRefCounted(type); - if ( - !ref && - type.kind !== "f64" && - type.kind !== "date" && - type.kind !== "procStream" && - type.kind !== "bool" - ) + if (!ref && type.kind !== "f64" && type.kind !== "procStream" && type.kind !== "bool") return null; this.remaining -= 48; const B = host.B; diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 044cb79ae..7c17a94f4 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -233,6 +233,10 @@ export class LlWalkers { this.host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`); B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`); break; + case "date": + this.host.declare(`declare void @scr_jb_put_date(ptr, ptr)`); + B.line(`call void @scr_jb_put_date(ptr %b, ptr %v) ; toJSON(): ISO text, null if invalid`); + break; case "dyn": // A checked-dynamic value serializes itself (runtime walker). // Bare stringify of dyn stays frontend-fenced, and overflow @@ -924,6 +928,17 @@ export class LlWalkers { case "string": this.putScrStr(B, buf, this.unionPeek(B, u, unionId)); break; + case "date": { + host.declare(`declare ptr @scr_date_object_to_string(ptr)`); + host.declare(`declare void @scr_str_release(ptr)`); + const text = B.tmp(); + B.line( + `${text} = call ptr @scr_date_object_to_string(ptr ${this.unionPeek(B, u, unionId)})`, + ); + this.putScrStr(B, buf, text); + B.line(`call void @scr_str_release(ptr ${text})`); + break; + } case "f64": { host.declare(`declare double @scr_union_get_f64(ptr)`); host.declare(`declare void @scr_jb_put_number(ptr, double)`); diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 625ee93af..2fd8b6fb0 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -656,7 +656,7 @@ export function genericSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiag export function indexSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic { return { code: "SC2006", - message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown')`, + message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown')`, loc, milestone: "M2", hint: "string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering", diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index a598f4ee6..0204386ab 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -53,7 +53,13 @@ export function lowerDateCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (call.questionDotToken || access.questionDotToken) return null; + // An optional chain lowers the call again once it has narrowed the + // receiver (Lowerer.chainHandled). + if ( + (call.questionDotToken && !lowerer.chainHandled.has(call)) || + (access.questionDotToken && !lowerer.chainHandled.has(access)) + ) + return null; const loc = locOf(call); const member = lowerer.stdlibGlobalMember(access, "Date"); if (member === "now" || member === "parse" || member === "UTC") diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index dacb4d324..c9fd954b5 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -611,7 +611,6 @@ export function checkDefaultParamBodyType( if ( bodyType.kind === "void" || bodyType.kind === "regex" || - bodyType.kind === "date" || bodyType.kind === "dyn" || isUnitType(bodyType) ) { diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 86f214bc1..edf65ab2b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -2358,10 +2358,11 @@ function reduceHelper( * the left element first. Undefined values sink without reaching the user * comparator. The callback sequence differs from V8's TimSort, so exact * order and count parity are not claimed; results agree for consistent - * comparators. Without a comparator, string, number and boolean arrays use - * native stable ordering: their String conversions cannot call user code. - * Strings compare UTF-16 units; numbers compare their decimal spellings - * ([10, 9, 1] → [1, 10, 9]). Other element types still need a comparator. */ + * comparators. Without a comparator, string, number, boolean and Date + * arrays use native stable ordering: their String conversions cannot call + * user code. Strings compare UTF-16 units; numbers compare their decimal + * spellings ([10, 9, 1] → [1, 10, 9]); Dates compare their toString() text. + * Other element types still need a comparator. */ export function lowerArraySortCall( lowerer: Lowerer, call: ts.CallExpression, @@ -2374,7 +2375,7 @@ export function lowerArraySortCall( const copyFirst = method === "toSorted"; if ( call.arguments.nodes.length === 0 && - (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool") + (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool" || elem.kind === "date") ) { return { kind: "arrIntrinsic", @@ -2389,7 +2390,7 @@ export function lowerArraySortCall( lowerer.noLowering( `.${method} with ${call.arguments.nodes.length} arguments`, call, - "default ordering supports string[], number[] and boolean[]; other element types need a comparator", + "default ordering supports string[], number[], boolean[] and Date[]; other element types need a comparator", ); } const receiver = lowerer.lowerExpr(access.expression); diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda..8a736e75f 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1066,14 +1066,25 @@ export function lowerArrayMethodCall( (lowerer.unions .get(elem.unionId) ?.arms.every( - (a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + (a) => + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false); - if (elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && !joinableUnion) { + if ( + elem.kind !== "f64" && + elem.kind !== "string" && + elem.kind !== "bool" && + elem.kind !== "date" && + !joinableUnion + ) { lowerer.unsupported( "SC1090", call, - "'.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS)", + "'.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS)", ); } const receiver = lowerer.lowerExpr(access.expression); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 9702a6dd9..ed4771b05 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -759,10 +759,21 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr (lowerer.unions .get(elem.unionId) ?.arms.every( - (a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + (a) => + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false); - if (elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool" || joinableUnion) { + if ( + elem.kind === "f64" || + elem.kind === "string" || + elem.kind === "bool" || + elem.kind === "date" || + joinableUnion + ) { const sep: IrExpr = { kind: "strLit", value: ",", type: STRING, loc: e.loc }; return { kind: "arrIntrinsic", @@ -2166,14 +2177,18 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc }; - // A Date converts to a new checked-dynamic Date on each crossing, so - // comparisons keep their Date fence instead of comparing copies. - if ( - left.type.kind !== "date" && - right.type.kind !== "date" && - lowerer.dynConvertible(left.type) && - lowerer.dynConvertible(right.type) - ) { + // Dates compare by time value, the number hint of ToPrimitive. + if (left.type.kind === "date" && right.type.kind === "date") { + const time = (d: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "date.getTime", + args: [d], + type: F64, + loc, + }); + return { kind: "bin", op: cmpOp, left: time(left), right: time(right), type: BOOL, loc }; + } + if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { return { kind: "libCall", fn: "dyn.compare", @@ -2387,6 +2402,7 @@ export function lowerStrictEquality( idLeft.type.kind === "map" || idLeft.type.kind === "set" || idLeft.type.kind === "regex" || + idLeft.type.kind === "date" || idLeft.type.kind === "url" || idLeft.type.kind === "object" || idLeft.type.kind === "record" || @@ -2423,14 +2439,7 @@ export function lowerStrictEquality( loc, }; } - // A Date converts to a new checked-dynamic Date on each crossing; its - // identity has no representation, so equality keeps the fence. - if ( - idLeft.type.kind !== "date" && - idRight.type.kind !== "date" && - lowerer.dynConvertible(idLeft.type) && - lowerer.dynConvertible(idRight.type) - ) { + if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) { return { kind: "dynScalarEq", left: lowerer.coerceToExpected(idLeft, DYN), diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index e264e0a3a..5079f3c9e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1415,7 +1415,7 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const arms = mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; for (const a of arms) { - if (a.kind === "map" || a.kind === "regex" || a.kind === "date" || a.kind === "generator") { + if (a.kind === "map" || a.kind === "regex" || a.kind === "generator") { return null; } byKey.set(typeKey(a), a); @@ -3971,7 +3971,6 @@ export function collectClassShapeInner( declared.kind !== "union" && declared.kind !== "dyn" && declared.kind !== "map" && - declared.kind !== "date" && declared.kind !== "generator" && declared.kind !== "void" && declared.kind !== "caught"; @@ -10324,8 +10323,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (arg.type.kind === "string") { return { kind: "libCall", fn: "date.newString", args: [arg], type: DATE_T, loc }; } + // new Date(date): a new Date object with the same time value. if (arg.type.kind === "date") { - return arg; + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc }; + return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc }; } lowerer.noLowering( `new Date of '${lowerer.fmt(arg.type)}' values`, @@ -10528,7 +10529,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { expr, `Map values of type '${lowerer.checker.typeToString(targs[1])}' ` + `(Map values must be number, string, boolean, records, class instances, functions, ` + - `typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`, + `typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values)`, ); } lowerer.badType(expr, tsType); diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 80463fa6c..f837cc0af 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -333,7 +333,7 @@ export function stepDrivable(genT: GenType): boolean { /** Whether a yield channel's values have an OUT-slot form `value` reads. */ function hasOutSlot(yt: IrType): boolean { - return yt.kind === "f64" || yt.kind === "date" || yt.kind === "bool" || isRefCounted(yt); + return yt.kind === "f64" || yt.kind === "bool" || isRefCounted(yt); } /** `step` and `value` resumes of a generator-typed borrowed read. */ diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ea5ef72ac..750057bf9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6687,7 +6687,7 @@ function lowerDynSwitch( loc: locOf(expression), }; } - if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) { + if (!lowerer.dynConvertible(test.type)) { lowerer.unsupported( "SC1100", expression, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 35514d992..baafd230a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3204,10 +3204,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } const collectionView = mapCollectionView(widened, ctx); if (collectionView) return collectionView; - // Date: a TimeClip'd epoch-millisecond scalar in the static runtime. - // This supports stored/passed values and the read-only getter slice; - // identity and mutation stay fenced because the scalar deliberately - // carries no object identity. + // Date: a refcounted object holding its TimeClip'd time value (ScrDate), + // shared with the checked-dynamic Date handle, so it keeps its identity + // through every slot and through `unknown`. Setters stay fenced. if (isStdlibInterface("Date")) return DATE_T; // RegExp: a reference to the lib RegExp interface — provenance, not the // name (a user's own `interface RegExp` maps as a record). Regex values @@ -5025,7 +5024,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (a) => a.kind === "void" || a.kind === "union" || - a.kind === "date" || a.kind === "dyn" || // A generator arm only comes from an Iterable (an optional // one keeps its unit arms beside the iterable union's). @@ -6558,7 +6556,7 @@ export function genResultRecord( for (const a of unions.get(t.unionId)?.arms ?? []) byKey.set(typeKey(a), a); return true; } - if (t.kind === "regex" || t.kind === "date" || t.kind === "generator") { + if (t.kind === "regex" || t.kind === "generator") { return false; // no legal union arm exists for these kinds } byKey.set(typeKey(t), t); @@ -6647,15 +6645,15 @@ export function withUnitArm( if (t.kind === "union") { const def = unions.get(t.unionId); if (!def) return null; - if (def.arms.some((a) => a.kind === "date")) return null; if (def.arms.some((a) => a.kind === kind)) return t; const arms = [...def.arms, unit]; + // Date and Promise arms beside other data arms have no union home. + if (!unionContainerArmsOk(arms)) return null; arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); return { kind: "union", unionId: unions.transform(def, arms) }; } if ( t.kind === "void" || - t.kind === "date" || t.kind === "dyn" || // A bare unit field type cannot occur (units live only inside unions), // but guard against constructing a single-arm union from one. @@ -8245,7 +8243,6 @@ function armHasUnionHome(arm: IrType): boolean { case "void": case "union": case "regex": - case "date": case "generator": case "dyn": return false; diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index d62c9c83c..30fe6f50e 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -57,7 +57,8 @@ describe("IR kind sets", () => { test("distinguishes pointer values from object-like scalars", () => { expect(POINTER_KINDS.has("record")).toBe(true); - expect(POINTER_KINDS.has("date")).toBe(false); + expect(POINTER_KINDS.has("date")).toBe(true); + expect(POINTER_KINDS.has("procStream")).toBe(false); }); test("distinguishes typed-rest closure ABIs", () => { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 12400fee9..5afe2b09b 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -440,6 +440,7 @@ const POINTER_KIND_LIST = [ "url", "searchParams", "symbol", + "date", ...POINTER_HANDLE_KINDS, "func", "object", @@ -463,7 +464,7 @@ export const POINTER_KINDS = irKindSet(POINTER_KIND_LIST); export const RUNTIME_RC_STEMS: Record = { f64: "", bigint: "scr_bigint", - date: "", + date: "scr_date", string: "scr_str", bool: "", array: "scr_arr", @@ -645,6 +646,7 @@ export function isSupportedArrayElem(t: IrType): boolean { case "func": case "promise": case "regex": + case "date": case "child": case "netServer": // fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable @@ -689,7 +691,8 @@ export function isIdentityCollectionKey(t: IrType): boolean { // functions intern one immortal closure, inner closures allocate once // per evaluation), so pointer identity is JS function identity. // Closures are cycle-headered and traced like records. - t.kind === "func" + t.kind === "func" || + t.kind === "date" ); } @@ -797,6 +800,9 @@ export function isSupportedMapValue(t: IrType): boolean { // the pre-settle closures. case "secureCtx": return true; + // A Date object: immutable, holds no references. + case "date": + return true; // A promise (Map> — the SNI callback's // in-flight dedupe map): refcounted, with trace/cycle support already // in place (scr_promise_trace_v — the race machinery's refcounting). @@ -849,8 +855,9 @@ export function funcOf(params: IrType[], ret: IrType): IrType { * promises nullable-only; declared maybe-promise unions are admitted by * maybePromiseUnionArmsOk. */ export function unionContainerArmsOk(arms: IrType[]): boolean { - return arms.every( - (a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)), + return ( + arms.every((a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b))) && + dateUnionArmsOk(arms) ); } @@ -861,7 +868,13 @@ export function unionContainerArmsOk(arms: IrType[]): boolean { * non-thenable path). Data arms must not be thenable; the type mapper * checks that against the checker types before minting such a union. */ export function maybePromiseUnionArmsOk(arms: IrType[]): boolean { - return arms.filter((a) => a.kind === "promise").length <= 1; + return arms.filter((a) => a.kind === "promise").length <= 1 && dateUnionArmsOk(arms); +} + +/** A Date arm has a union home beside undefined and null arms only: no + * narrowing contract tells it from other data arms yet. */ +function dateUnionArmsOk(arms: IrType[]): boolean { + return arms.every((a, i) => a.kind !== "date" || arms.every((b, j) => j === i || isUnitType(b))); } /** Canonical, injective text form of an IrType — the building block of @@ -3975,6 +3988,9 @@ function isJsonSafeAt( case "string": case "bool": return true; + // JSON.stringify writes a Date's toJSON(); JSON text never parses to one. + case "date": + return stringify; case "array": return isJsonSafeAt( t.elem, @@ -4073,7 +4089,6 @@ function isJsonSafeAt( // Maps are not JSON (JSON.stringify(new Map()) is "{}" in Node — an // empty-object husk nobody wants; stringify/dynCheck reject instead). case "map": - case "date": // URLSearchParams stringifies as the same "{}" husk — rejected; use // sp.toString() instead. case "searchParams": diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 63aa8ff5c..230d2385d 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -55,6 +55,7 @@ import { typeEquals, typeKey, maybePromiseUnionArmsOk, + unionContainerArmsOk, VOID, } from "./ir.js"; import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; @@ -801,7 +802,6 @@ export function validateModule(mod: IrModule): IrValidationError[] { arm.kind === "void" || arm.kind === "union" || arm.kind === "dyn" || - arm.kind === "date" || (arm.kind === "generator" && iterableArmsElem(u.arms) === null) ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); @@ -812,6 +812,9 @@ export function validateModule(mod: IrModule): IrValidationError[] { loc: noLoc, }); } + if (arm.kind === "date" && !unionContainerArmsOk(u.arms)) { + errors.push({ message: `union ${u.id}: date arm ${i} beside non-unit arms`, loc: noLoc }); + } if (arm.kind === "record" && !recordsById.has(arm.shapeId)) { errors.push({ message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`, @@ -1505,6 +1508,7 @@ function validateFunction( e.left.type.kind === "map" || e.left.type.kind === "set" || e.left.type.kind === "regex" || + e.left.type.kind === "date" || e.left.type.kind === "url" || e.left.type.kind === "object" || e.left.type.kind === "record" || @@ -2370,9 +2374,13 @@ function validateFunction( (e.method === "sortPrimitive" || e.method === "toSortedPrimitive") && elem.kind !== "string" && elem.kind !== "f64" && - elem.kind !== "bool" + elem.kind !== "bool" && + elem.kind !== "date" ) { - err(`arrIntrinsic ${e.method} requires primitive elements, got ${elem.kind}`, e.loc); + err( + `arrIntrinsic ${e.method} requires primitive or Date elements, got ${elem.kind}`, + e.loc, + ); } if (e.method === "sortValues" || e.method === "sortDense") { const arms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined; @@ -2454,13 +2462,18 @@ function validateFunction( elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && + elem.kind !== "date" && !( elem.kind === "union" && (unions .get(elem.unionId) ?.arms.every( (a) => - a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false) ) diff --git a/packages/compiler/test/collection-keys.test.ts b/packages/compiler/test/collection-keys.test.ts index d73655ef1..ec049f23c 100644 --- a/packages/compiler/test/collection-keys.test.ts +++ b/packages/compiler/test/collection-keys.test.ts @@ -90,7 +90,7 @@ test("mixed-value unions select value equality while unsupported arms remain fen expect(mapKeyKindNum(union, [reference, scalar])).toBe(7); expect(isSupportedSetElem(union, [reference, scalar])).toBe(true); } - for (const unsupported of [{ kind: "date" } as IrType, mapOf(STRING, F64), setOf(STRING)]) { + for (const unsupported of [{ kind: "regex" } as IrType, mapOf(STRING, F64), setOf(STRING)]) { expect(isSupportedMapKey(unsupported)).toBe(false); } }); @@ -171,7 +171,7 @@ test.each(["map", "set"] as const)("validator checks the arms behind a %s key un expect(validateModule(mod)).toEqual([]); mod.unions![0]!.arms = [reference, F64]; expect(validateModule(mod)).toEqual([]); - mod.unions![0]!.arms = [reference, { kind: "date" }]; + mod.unions![0]!.arms = [reference, { kind: "regex" }]; expect( validateModule(mod).some((error) => error.message.includes( diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index 7f1a1ef78..7f5e9f3c9 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -8,10 +8,15 @@ test("hand-built fib module validates", () => { expect(validateModule(fibModule)).toEqual([]); }); -test("validator rejects Date union arms unsupported by the backends", () => { +test("validator rejects Date union arms beside other data arms", () => { const bad = structuredClone(fibModule); - bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; - expect(validateModule(bad).map((e) => e.message)).toContain("union u-date: arm 0 is date"); + bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }]; + expect(validateModule(bad).map((e) => e.message)).toContain( + "union u-date: date arm 0 beside non-unit arms", + ); + const nullable = structuredClone(fibModule); + nullable.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; + expect(validateModule(nullable)).toEqual([]); }); test("fib module JSON round-trips", () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json new file mode 100644 index 000000000..c2497d502 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5965-date-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json deleted file mode 100644 index cf6ad7398..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/date-union-fences.ts" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index f3e216a36..a3b95146c 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -3058,10 +3058,12 @@ typedef struct { typedef struct { ScrElemKind kind; const ScrNumberSortKey *numbers; + ScrStr *const *texts; /* reference elements: each element's String() text */ } ScrPrimitiveOrder; static int scr_arr_primitive_compare(uint64_t left, uint64_t right, const ScrPrimitiveOrder *order) { + if (order->texts) return scr_str_cmp_u16(order->texts[left], order->texts[right]); if (order->kind == SCR_ELEM_STR) return scr_str_cmp_u16(scr_slot_to_ptr(left), scr_slot_to_ptr(right)); if (order->kind == SCR_ELEM_BOOL) return (left > right) - (left < right); @@ -3149,10 +3151,28 @@ static void scr_arr_primitive_order(uint64_t *values, size_t count, if (runs != local_runs) free(runs); } +static ScrArr *scr_arr_sort_default(ScrArr *a, bool copy, ScrStr *(*text)(void *)); + +static ScrArr *scr_arr_new_like(const ScrArr *a, size_t cap) { + return a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), cap) : scr_arr_new(a->elem, cap); +} + ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->elem != SCR_ELEM_STR && a->elem != SCR_ELEM_F64 && a->elem != SCR_ELEM_BOOL) scr_trap("scriptc: invalid primitive array ordering\n"); - if (a->len == 0) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); + return scr_arr_sort_default(a, copy, NULL); +} + +/* The default ordering of an array of reference elements that cannot call + * user code while converting to strings (Dates): `text` answers each + * element's String() text (+1), computed once per element. */ +ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *)) { + if (a->elem != SCR_ELEM_REF) scr_trap("scriptc: invalid text array ordering\n"); + return scr_arr_sort_default(a, copy, text); +} + +static ScrArr *scr_arr_sort_default(ScrArr *a, bool copy, ScrStr *(*text)(void *)) { + if (a->len == 0) return copy ? scr_arr_new_like(a, 0) : scr_arr_retain(a); size_t dense = a->len < a->cap ? a->len : a->cap; size_t count = 0, undefined_count = 0; const uint8_t *states = scr_arr_scan_states(a); @@ -3184,14 +3204,35 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { values[i] = i; } } - ScrPrimitiveOrder order = {a->elem, numbers}; + ScrStr **texts = NULL; + uint64_t *originals = NULL; + if (text && count > 1) { + if (count > SIZE_MAX / sizeof(*texts)) scr_arr_oom(); + texts = malloc(count * sizeof(*texts)); + originals = malloc(count * sizeof(*originals)); + if (!texts || !originals) scr_arr_oom(); + for (size_t i = 0; i < count; i++) { + originals[i] = values[i]; + texts[i] = text(scr_slot_to_ptr(values[i])); + values[i] = i; + } + } + ScrPrimitiveOrder order = {a->elem, numbers, texts}; scr_arr_primitive_order(values, count, &order); if (numbers) { for (size_t i = 0; i < count; i++) values[i] = numbers[values[i]].value; free(numbers); } + if (texts) { + for (size_t i = 0; i < count; i++) { + values[i] = originals[values[i]]; + scr_str_release(texts[i]); + } + free(texts); + free(originals); + } size_t present = copy ? a->len : count + undefined_count; - ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; + ScrArr *out = copy ? scr_arr_new_like(a, present) : a; if (!copy && !states) { /* Values only, in place. */ if (count) scr_arr_put_slots(a, 0, count, values); diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index e13acb233..2aa1f4c84 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -576,6 +576,10 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src); ScrArr *scr_arr_reverse(ScrArr *a); /* Default primitive String ordering; stable, borrowed receiver, owned result. */ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy); +/* Default ordering of reference elements by their String() text, which + * `text` answers (+1) for each element once (Date arrays). Like + * scr_arr_sort_primitive otherwise. */ +ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *)); /* Stable comparator sort of the first `count` slots of a private, compacted * snapshot (every slot a value; f64 or reference elements). `f` is a borrowed * closure of `arity` (0..2) element parameters returning a number. Elements diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..c86d2a71d 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -767,6 +767,8 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); ScrStr *scr_dyn_object_tag_of(const ScrDyn *d); ScrDyn *scr_dyn_native_date_from_ms(double ms); +typedef struct ScrDate ScrDate; +ScrDyn *scr_dyn_native_date(ScrDate *date); double scr_dyn_native_date_value(const ScrDyn *value); ScrDyn *scr_dyn_native_regex(ScrRegex *value); bool scr_dyn_native_regex_is(const ScrDyn *value); @@ -1406,6 +1408,9 @@ ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, cons * prints as "Uncaught TypeError: ..." and exits 1). This is * scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got); +/* The Date object a checked-dynamic Date handle holds, +1, or NULL with + * the check failure pending. */ +ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path); /* Matchers and builders of recursive shapes (records and arrays whose type * reaches itself) track the dyn values they visit, so cyclic values match diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h index e3077703e..de4510264 100644 --- a/packages/runtime/src/scr_json.h +++ b/packages/runtime/src/scr_json.h @@ -162,6 +162,10 @@ void scr_jb_put_number(ScrJsonBuf *b, double v); * as \u00XX, lone surrogates as \udXXX, everything else (UTF-8 included) * verbatim — exactly the JS JSON.stringify escape set. */ void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s); +/* JSON.stringify of a Date object: toJSON()'s ISO text, null if invalid + * (scr_lib.c). */ +typedef struct ScrDate ScrDate; +void scr_jb_put_date(ScrJsonBuf *b, const ScrDate *date); /* JSON-serialize a dyn value into the buffer — the sc_jw_* walker for the * dyn leaves the compiler cannot type: undefined prints null (an array diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index c17f44f9d..18d9c39e5 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12549,6 +12549,39 @@ static ScrStr *scr_date_text(double ms, int mode) { } ScrStr *scr_date_to_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_FULL); } + +/* JSON.stringify of a Date object: its toJSON(), the quoted + * toISOString() text, or null for an invalid date. */ +void scr_jb_put_date(ScrJsonBuf *b, const ScrDate *date) { + if (isnan(date->ms)) { + scr_jb_puts(b, "null"); + return; + } + ScrStr *iso = scr_date_to_iso(date->ms); + scr_jb_put_json_str(b, iso); + scr_str_release(iso); +} + +/* String(date) of a Date object: Date arrays' default ordering key. +1. */ +ScrStr *scr_date_object_to_string(void *date) { + return scr_date_text(((ScrDate *)date)->ms, SCR_DATE_TEXT_FULL); +} + +/* Array.prototype.join over Date objects: each element's String() text, + * holes empty. Both borrowed; +1. */ +ScrStr *scr_date_arr_join(ScrArr *dates, const ScrStr *sep) { + ScrJsonBuf b; + scr_jb_init(&b); + double len = scr_arr_len(dates); + for (double i = 0; i < len; i++) { + if (i > 0) scr_jb_put_str(&b, sep); + if (scr_arr_state(dates, i) != 1) continue; + ScrStr *text = scr_date_object_to_string(scr_arr_peek_ref(dates, i)); + scr_jb_put_str(&b, text); + scr_str_release(text); + } + return scr_jb_finish(&b); +} ScrStr *scr_date_to_date_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_DATE); } ScrStr *scr_date_to_time_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_TIME); } @@ -13009,11 +13042,29 @@ ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { return out; } -/* JS Date storage owns an identity; the typed read-only Date ABI continues - * to use milliseconds and explicitly extracts them for native getters. */ -typedef struct { size_t rc; double milliseconds; } ScrNativeDate; -static void *scr_native_date_retain(void *ptr) { ((ScrNativeDate *)ptr)->rc++; return ptr; } -static void scr_native_date_release(void *ptr) { if (--((ScrNativeDate *)ptr)->rc == 0) free(ptr); } +/* A Date object: one time value, and its identity. Compiled code and + * checked-dynamic Date handles share these objects, so a Date keeps its + * identity across `unknown` conversions. */ +ScrDate *scr_date_new(double ms) { + ScrDate *date = malloc(sizeof *date); + if (!date) scr_trap("scriptc: out of memory\n"); + date->rc = 1; + date->ms = ms; + return date; +} + +ScrDate *scr_date_retain(ScrDate *date) { + if (date->rc != SIZE_MAX) date->rc++; + return date; +} + +void scr_date_release(ScrDate *date) { + if (!date || date->rc == SIZE_MAX) return; + if (--date->rc == 0) free(date); +} + +void *scr_date_retain_v(void *p) { return scr_date_retain(p); } +void scr_date_release_v(void *p) { scr_date_release(p); } bool scr_dyn_native_date_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; @@ -13124,13 +13175,25 @@ static ScrStr *scr_date_class_name(const ScrDyn *value) { } double scr_dyn_native_date_value(const ScrDyn *value) { - if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + if (scr_dyn_native_date_is(value)) return ((ScrDate *)value->v.handle.ptr)->ms; double time; if (scr_dyn_date_class_time(value, &time)) return time; scr_dyn_check_fail(NULL, "Date", value); return NAN; } +/* The Date object a checked-dynamic Date value holds, +1: the Date itself, + * or a new Date with the time value of an instance of a class declared + * `extends Date`. NULL with the check failure pending for any other + * value. */ +ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path) { + if (scr_dyn_native_date_is(value)) return scr_date_retain(value->v.handle.ptr); + double time; + if (scr_dyn_date_class_time(value, &time)) return scr_date_new(time); + scr_dyn_check_fail(path, "Date", value); + return NULL; +} + /* Method names without another use in this file, in named arrays (see * scr_tz_posix_ids). */ static const char scr_date_to_date_string_name[] = "toDateString"; @@ -13139,7 +13202,7 @@ static const char scr_date_to_time_string_name[] = "toTimeString"; static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)self; (void)what; - double ms = ((ScrNativeDate *)ptr)->milliseconds; + double ms = ((ScrDate *)ptr)->ms; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || !strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) { @@ -13162,8 +13225,8 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho if (!strcmp(method, "setTime")) { double time = NAN; if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL; - ((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time); - return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds); + ((ScrDate *)ptr)->ms = scr_date_new_ms(time); + return scr_dyn_new_num(((ScrDate *)ptr)->ms); } #define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms)) DATE_GET("getFullYear", scr_date_get_full_year_local); @@ -13209,7 +13272,7 @@ double scr_dyn_date_arguments_time(const ScrDyn *arguments) { ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments) : scr_dyn_native_date_new_value(arguments); if (!date) return NAN; - double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds; + double time = ((ScrDate *)date->v.handle.ptr)->ms; scr_dyn_release(date); return time; } @@ -13274,10 +13337,10 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL) : scr_dyn_new_null(); } else { - ScrNativeDate date = { 1, time }; + ScrDate date = { 1, time }; result = scr_native_date_invoke(&date, self, method, args, argc, method); - if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) { - ScrDyn *stored = scr_dyn_new_num(date.milliseconds); + if (result && date.ms != time && !(isnan(date.ms) && isnan(time))) { + ScrDyn *stored = scr_dyn_new_num(date.ms); ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored); scr_dyn_release(stored); if (!set) { @@ -13330,19 +13393,22 @@ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { return NULL; } -/* A Date handle holding the time value `ms` (already TimeClip'd). */ -ScrDyn *scr_dyn_native_date_from_ms(double ms) { +/* A checked-dynamic handle for the Date object `date` (borrowed; the + * handle retains it), so the value stays the same object. */ +ScrDyn *scr_dyn_native_date(ScrDate *date) { static const ScrDynHandleOps ops = { - "Date", &scr_native_date_retain, &scr_native_date_release, &scr_native_date_invoke, + "Date", &scr_date_retain_v, &scr_date_release_v, &scr_native_date_invoke, &scr_native_date_get, NULL, NULL, NULL, }; scr_dyn_handle_install(SCR_DYNH_DATE, &ops); - ScrNativeDate *date = malloc(sizeof *date); - if (!date) scr_trap("scriptc: out of memory\n"); - date->rc = 1; - date->milliseconds = ms; - ScrDyn *result = scr_dyn_new_handle(date, SCR_DYNH_DATE); - scr_native_date_release(date); + return scr_dyn_new_handle(date, SCR_DYNH_DATE); +} + +/* A Date handle holding the time value `ms` (already TimeClip'd). */ +ScrDyn *scr_dyn_native_date_from_ms(double ms) { + ScrDate *date = scr_date_new(ms); + ScrDyn *result = scr_dyn_native_date(date); + scr_date_release(date); return result; } diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 78d19614b..a71b0ccfd 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -111,6 +111,18 @@ double scr_date_now(void); /* integer ms since epoch, like Node */ extern SCR_TL const double *scr_date_mock_now; double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */ double scr_date_get_time(double ms); +/* A Date object: the TimeClip'd time value in milliseconds (NaN for an + * invalid date). Dates are immutable; their identity is the pointer. + * Retain/release follow the runtime's RC conventions (SIZE_MAX immortal). */ +typedef struct ScrDate { + size_t rc; + double ms; +} ScrDate; +ScrDate *scr_date_new(double ms); /* +1 */ +ScrDate *scr_date_retain(ScrDate *date); +void scr_date_release(ScrDate *date); +void *scr_date_retain_v(void *p); +void scr_date_release_v(void *p); /* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside * 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or * |ms| > 8.64e15 and returns NULL; +1 otherwise. */ @@ -150,6 +162,11 @@ double scr_date_get_timezone_offset(double ms); ScrStr *scr_date_to_string(double ms); ScrStr *scr_date_to_date_string(double ms); ScrStr *scr_date_to_time_string(double ms); +/* String() of a Date object (ScrDate *), and Array.prototype.join over an + * array of Date objects (holes empty). +1. */ +ScrStr *scr_date_object_to_string(void *date); +struct ScrArr; +ScrStr *scr_date_arr_join(struct ScrArr *dates, const ScrStr *sep); /* The long time zone name Node prints after the GMT offset for the host * zone at time value `ms`; `isdst` is tm_isdst at `ms`, or -1 when not * known. Borrowed; valid until the next call on the same thread. */ diff --git a/tests/corpus/5965-date-objects.ts b/tests/corpus/5965-date-objects.ts new file mode 100644 index 000000000..c4bb01fb9 --- /dev/null +++ b/tests/corpus/5965-date-objects.ts @@ -0,0 +1,115 @@ +// Dates are objects with identity: arrays of Dates, `Date | undefined` +// and `Date | null` slots, === by identity, includes/indexOf/lastIndexOf, +// at/find/pop answering undefined, Map and Set keys and values, join, +// String() and default sort through toString(), and Dates in records, +// tuples, class fields, closures, async results and generators. +export {}; +process.env.TZ = "America/Chicago"; + +const base = new Date(Date.UTC(2026, 3, 1, 12)); +const same = base; +const copy = new Date(base); +const equalTime = new Date(base.getTime()); +console.log(base === same, base === copy, copy === equalTime, base !== copy); +console.log(copy.getTime() === base.getTime(), base < new Date(base.getTime() + 1), base >= copy); + +// Arrays. +const dates: Date[] = [base, new Date(Date.UTC(2025, 0, 1)), new Date(Date.UTC(2026, 0, 1))]; +console.log(dates.length, dates.includes(base), dates.includes(copy), dates.indexOf(dates[2]!)); +console.log(dates.indexOf(equalTime), dates.lastIndexOf(base), dates.indexOf(base, 1)); +console.log(dates.join(" | ")); +console.log(String(dates)); +console.log(`${dates}`); +console.log([...dates].sort().map((d) => d.toISOString())); +console.log(dates.toSorted((a, b) => a.getTime() - b.getTime()).map((d) => d.getUTCFullYear())); +const first = dates.at(0); +const missing = dates.at(10); +console.log(first === base, missing, missing === undefined, first?.toISOString(), missing?.toISOString()); +const found = dates.find((d) => d.getUTCFullYear() === 2025); +console.log(found === dates[1], dates.find((d) => d.getTime() < 0), dates.findLast((d) => d > base)); +const stack = [...dates]; +console.log(stack.pop() === dates[2], stack.pop()?.getUTCFullYear(), stack.pop() === base, stack.pop()); +const latest = dates.reduce((m, d) => (d > m ? d : m)); +console.log(latest === base, dates.filter((d) => d < base).length, dates.map((d) => d.getUTCMonth())); +const grid: Date[][] = [[base], [copy, base]]; +console.log(grid[1]!.indexOf(base), grid.flat().length, grid.flat().includes(copy)); +const holes: (Date | undefined)[] = [base, undefined, copy]; +console.log(holes.join(","), holes.indexOf(undefined), holes.filter((d) => d !== undefined).length); + +// Nullable slots. +let maybe: Date | null = null; +console.log(maybe === null, maybe); +maybe = base; +console.log(maybe === base, maybe.getUTCHours()); +function stamp(d?: Date): string { + return d === undefined ? "none" : d.toISOString(); +} +function withDefault(d: Date = new Date(0)): number { + return d.getTime(); +} +console.log(stamp(), stamp(base), withDefault(), withDefault(base) === base.getTime()); + +// Collections. +const notes = new Map([[base, "base"]]); +notes.set(copy, "copy"); +console.log(notes.get(base), notes.get(copy), notes.get(equalTime), notes.size); +const byName = new Map([["start", base]]); +console.log(byName.get("start") === base, byName.get("end")); +const seen = new Set([base, same, copy]); +console.log(seen.size, seen.has(base), seen.has(equalTime)); + +// Records, tuples, classes and closures. +interface Event { + name: string; + at: Date; + ends?: Date; +} +const events: Event[] = [ + { name: "a", at: base }, + { name: "b", at: copy, ends: base }, +]; +console.log(events.map((e) => `${e.name}@${e.at.toISOString()} ends=${e.ends?.toISOString() ?? "-"}`)); +console.log(events[1]!.ends === events[0]!.at, events[1]!.at === events[0]!.at); +const pair: [Date, string] = [base, "x"]; +console.log(pair[0] === base, pair[1]); +class Window { + from: Date; + to: Date | null = null; + later!: Date; + constructor(from: Date) { + this.from = from; + } + span(): number { + return this.to === null ? 0 : this.to.getTime() - this.from.getTime(); + } +} +const w = new Window(base); +w.later = copy; +console.log(w.span(), w.later === copy); +w.to = new Date(base.getTime() + 60000); +console.log(w.span(), w.to === w.from); +let captured: Date | undefined; +const remember = (d: Date): void => { + captured = d; +}; +remember(base); +console.log(captured === base, captured?.getUTCDate()); + +// Async results, generators and unknown. +async function later(d: Date): Promise { + await null; + return new Date(d.getTime() + 1); +} +const next = await later(base); +console.log(next.getTime() - base.getTime(), next === base); +function* ticks(start: Date, n: number): Generator { + for (let i = 0; i < n; i++) yield new Date(start.getTime() + i * 1000); +} +console.log([...ticks(base, 3)].map((d) => d.getUTCSeconds())); +const held: unknown = base; +console.log((held as Date) === base, held instanceof Date, String(held) === String(base)); +const bag: unknown = { at: base, list: dates }; +console.log(JSON.stringify(bag)); +console.log(JSON.stringify(events), JSON.stringify([base, new Date(NaN)])); +const byKey: { [k: string]: Date } = { boot: base }; +console.log(byKey["boot"] === base, byKey["missing"], Object.keys(byKey)); diff --git a/tests/diagnostics/component-fences.ts b/tests/diagnostics/component-fences.ts index 6d88f9af5..0d382bf28 100644 --- a/tests/diagnostics/component-fences.ts +++ b/tests/diagnostics/component-fences.ts @@ -5,9 +5,9 @@ const byFlag = new Map(); console.log(byFlag); -// Map values have no slot for Dates. -const stamps = new Map(); -console.log(stamps); +// Map values have no slot for URLSearchParams. +const queries = new Map(); +console.log(queries); // Boolean Set elements remain outside the supported element domain. const flags = new Set(); diff --git a/tests/diagnostics/date-union-fences.ts b/tests/diagnostics/date-union-fences.ts deleted file mode 100644 index 478df74d6..000000000 --- a/tests/diagnostics/date-union-fences.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Compiler-synthesized `T | undefined` slots must observe the same Date -// union fence as source-written unions. Each construct below used to reach -// IR validation as an SC9001 internal compiler error. - -function defaultDate(value: Date = new Date(0)): number { - return value.getTime(); -} -defaultDate(); - -function* dates(): Generator { - yield new Date(0); -} -for (const value of dates()) console.log(value.getTime()); - -class DeferredDate { - value!: Date; - initialize(): void { - this.value = new Date(0); - } -} -const deferred = new DeferredDate(); -deferred.initialize(); diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index 1fdc01675..d28333178 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -23,7 +23,6 @@ const fmt = new Intl.NumberFormat(); const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced const mutableDate = new Date(0); mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice -const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity const wr = new WeakRef({}); const px = new Proxy({ a: 1 }, {}); const buf = new ArrayBuffer(8); @@ -74,13 +73,6 @@ function pick(a: string | number, b: number | boolean): boolean { } const picked = pick(1, 2); -// Date values deliberately have no tagged-union payload yet. Nullable -// Date positions must fence in the frontend instead of reaching a backend -// scalar/ref mismatch. -function optionalDate(value: Date | undefined): number { - return value === undefined ? -1 : value.getTime(); -} -optionalDate(undefined); function throwDate(): void { throw new Date(0); // the exception cell cannot preserve Date's object kind } diff --git a/tests/diagnostics/utility-types.ts b/tests/diagnostics/utility-types.ts index c6d10648b..cad1fcca4 100644 --- a/tests/diagnostics/utility-types.ts +++ b/tests/diagnostics/utility-types.ts @@ -5,9 +5,7 @@ // fields + an overflow map; number keys canonicalize to their JS string // spelling; function/Map/Set/nested-record values ride the same store — // differential corpus); what stays rejected: -// - symbol-keyed index signatures (no lowering for symbol keys); -// - index-signature value types outside the overflow-map slice (Dates and -// other lib API objects, even when the value itself has a representation). +// - symbol-keyed index signatures (no lowering for symbol keys). // (Readonly bindings compile, holding the Date as a checked value. // Dot access/writes to index-signature keys compile now — the overflow // path in dot spelling — and self-referential mapped types intern as @@ -15,9 +13,5 @@ const bySymbol: { [s: symbol]: string } = {}; console.log(bySymbol); -const dateBag: { [k: string]: Date } = {}; -const boot = dateBag["boot"]; -console.log(boot); - const stamped: Readonly = new Date(0); console.log(stamped.getTime()); diff --git a/tests/harness/__snapshots__/class-interface-refinements.ts.txt b/tests/harness/__snapshots__/class-interface-refinements.ts.txt index 043cb3996..9d7d7b87f 100644 --- a/tests/harness/__snapshots__/class-interface-refinements.ts.txt +++ b/tests/harness/__snapshots__/class-interface-refinements.ts.txt @@ -39,7 +39,7 @@ class-interface-refinements.ts:17:13 - error SC2004: uses of 'changed' inherit t hint: the declaration of 'changed' did not compile — fix the diagnostic reported there and these sites clear with it -class-interface-refinements.ts:20:18 - error SC2006: values of type 'Indexed' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +class-interface-refinements.ts:20:18 - error SC2006: values of type 'Indexed' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') 19 | type Indexed = Box & { [name: string]: number }; 20 | function indexed(view: Indexed, key: string): number { return view[key]!; } diff --git a/tests/harness/__snapshots__/component-fences.ts.txt b/tests/harness/__snapshots__/component-fences.ts.txt index 5ed6ad6b9..45d476089 100644 --- a/tests/harness/__snapshots__/component-fences.ts.txt +++ b/tests/harness/__snapshots__/component-fences.ts.txt @@ -1,27 +1,27 @@ -component-fences.ts:9:7 - error SC2009: values of type 'Map' cannot be compiled: the Map shape is supported, but 'Date' values have no Map slot yet +component-fences.ts:9:7 - error SC2009: values of type 'Map' cannot be compiled: the Map shape is supported, but 'URLSearchParams' values have no Map slot yet - 8 | // Map values have no slot for Dates. - 9 | const stamps = new Map(); - | ^~~~~~ - 10 | console.log(stamps); + 8 | // Map values have no slot for URLSearchParams. + 9 | const queries = new Map(); + | ^~~~~~~ + 10 | console.log(queries); hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -component-fences.ts:9:16 - error SC1090: Map values of type 'Date' (Map values must be number, string, boolean, records, class instances, functions, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet +component-fences.ts:9:17 - error SC1090: Map values of type 'URLSearchParams' (Map values must be number, string, boolean, records, class instances, functions, typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values) is not supported yet - 8 | // Map values have no slot for Dates. - 9 | const stamps = new Map(); - | ^~~~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(stamps); + 8 | // Map values have no slot for URLSearchParams. + 9 | const queries = new Map(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | console.log(queries); -component-fences.ts:10:13 - error SC2004: uses of 'stamps' inherit the blocker on its declaration +component-fences.ts:10:13 - error SC2004: uses of 'queries' inherit the blocker on its declaration - 9 | const stamps = new Map(); - 10 | console.log(stamps); - | ^~~~~~ + 9 | const queries = new Map(); + 10 | console.log(queries); + | ^~~~~~~ 11 | - hint: the declaration of 'stamps' did not compile — fix the diagnostic reported there and these sites clear with it + hint: the declaration of 'queries' did not compile — fix the diagnostic reported there and these sites clear with it component-fences.ts:35:7 - error SC2009: values of type '() => WeakRef' cannot be compiled: the function shape is supported, but its return type 'WeakRef' does not compile diff --git a/tests/harness/__snapshots__/date-union-fences.ts.txt b/tests/harness/__snapshots__/date-union-fences.ts.txt deleted file mode 100644 index 1b504f23a..000000000 --- a/tests/harness/__snapshots__/date-union-fences.ts.txt +++ /dev/null @@ -1,56 +0,0 @@ -date-union-fences.ts:5:22 - error SC1090: parameter default values on 'Date'-typed parameters are not supported yet - - 4 | - 5 | function defaultDate(value: Date = new Date(0)): number { - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | return value.getTime(); - -date-union-fences.ts:8:1 - error SC2004: uses of 'defaultDate' inherit the blocker on its declaration - - 7 | } - 8 | defaultDate(); - | ^~~~~~~~~~~ - 9 | - - hint: the declaration of 'defaultDate' did not compile — fix the diagnostic reported there and these sites clear with it - -date-union-fences.ts:10:11 - error SC2009: values of type 'Generator' cannot be compiled: the Generator shape is supported, but this instantiation's type arguments are outside the supported set - - 9 | - 10 | function* dates(): Generator { - | ^~~~~ - 11 | yield new Date(0); - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -date-union-fences.ts:13:21 - error SC2004: uses of 'dates' inherit the blocker on its declaration - - 12 | } - 13 | for (const value of dates()) console.log(value.getTime()); - | ^~~~~ - 14 | - - hint: the declaration of 'dates' did not compile — fix the diagnostic reported there and these sites clear with it - -date-union-fences.ts:16:3 - error SC1090: definite assignment assertions on fields not assigned at the constructor's top level ('value!' defers the first assignment past construction — the field would hold garbage, not undefined, until it runs; assign it in the constructor or include undefined in its type) is not supported yet - - 15 | class DeferredDate { - 16 | value!: Date; - | ^~~~~ - 17 | initialize(): void { - -date-union-fences.ts:21:18 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - - 20 | } - 21 | const deferred = new DeferredDate(); - | ^~~~~~~~~~~~~~~~~~ - 22 | deferred.initialize(); - -date-union-fences.ts:22:1 - error SC2004: uses of 'deferred' inherit the blocker on its declaration - - 21 | const deferred = new DeferredDate(); - 22 | deferred.initialize(); - | ^~~~~~~~ - 23 | - - hint: the declaration of 'deferred' 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__/generics.ts.txt b/tests/harness/__snapshots__/generics.ts.txt index df6e552db..4dfc8019f 100644 --- a/tests/harness/__snapshots__/generics.ts.txt +++ b/tests/harness/__snapshots__/generics.ts.txt @@ -5,7 +5,7 @@ generics.ts:23:3 - error SC1090: generic nested functions (only top-level generi | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | return x; -generics.ts:30:10 - error SC1090: '.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet (instantiating 'joinAll' with ) +generics.ts:30:10 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet (instantiating 'joinAll' with ) 29 | function joinAll(a: T[]): string { 30 | return a.join(","); diff --git a/tests/harness/__snapshots__/index-signature-values.ts.txt b/tests/harness/__snapshots__/index-signature-values.ts.txt index bbbed7fe2..084d6a259 100644 --- a/tests/harness/__snapshots__/index-signature-values.ts.txt +++ b/tests/harness/__snapshots__/index-signature-values.ts.txt @@ -1,4 +1,4 @@ -index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') 7 | // A symbol-keyed signature has no lowering. 8 | const bySymbol: { [s: symbol]: number } = {}; @@ -14,7 +14,7 @@ index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: | ^~~~~~~ 14 | -index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') 17 | // answer per key). 18 | const dual: { [k: string]: string | number; [n: number]: number } = {}; diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index af1725323..c183f7cdc 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -64,104 +64,79 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen 24 | const mutableDate = new Date(0); 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity + 26 | const wr = new WeakRef({}); hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering -stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet +stdlib-fence.ts:26:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ - 27 | const wr = new WeakRef({}); - -stdlib-fence.ts:27:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet - - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - 27 | const wr = new WeakRef({}); + 26 | const wr = new WeakRef({}); | ^~ - 28 | const px = new Proxy({ a: 1 }, {}); + 27 | const px = new Proxy({ a: 1 }, {}); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -stdlib-fence.ts:27:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:26:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - 27 | const wr = new WeakRef({}); + 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice + 26 | const wr = new WeakRef({}); | ^~~~~~~~~~~~~~~ - 28 | const px = new Proxy({ a: 1 }, {}); + 27 | const px = new Proxy({ a: 1 }, {}); hint: deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead -stdlib-fence.ts:42:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet +stdlib-fence.ts:41:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet - 41 | const norm = "abc".normalize(); - 42 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower + 40 | const norm = "abc".normalize(); + 41 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 43 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) + 42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) -stdlib-fence.ts:44:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:43:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet - 43 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) - 44 | const localized = (1234.5).toLocaleString(); + 42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) + 43 | const localized = (1234.5).toLocaleString(); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 45 | // Unlowered call FORMS of lowered members. (push is fully variadic now — + 44 | // Unlowered call FORMS of lowered members. (push is fully variadic now — hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") -stdlib-fence.ts:50:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:49:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet - 49 | const joined = nums.join(); // the omitted separator lowers - 50 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced + 48 | const joined = nums.join(); // the omitted separator lowers + 49 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 51 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp) + 50 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp) hint: the thisArg parameter has no lowering — use an arrow function -stdlib-fence.ts:80:23 - error SC2009: values of type 'Date | undefined' cannot be compiled: the union shape is supported, but 'Date' arms have no home in a compiled union yet (no runtime narrowing test exists against sibling arms) +stdlib-fence.ts:77:9 - error SC1090: throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead) are not supported yet - 79 | // scalar/ref mismatch. - 80 | function optionalDate(value: Date | undefined): number { - | ^~~~~ - 81 | return value === undefined ? -1 : value.getTime(); + 76 | function throwDate(): void { + 77 | throw new Date(0); // the exception cell cannot preserve Date's object kind + | ^~~~~~~~~~~ + 78 | } - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -stdlib-fence.ts:83:1 - error SC2004: uses of 'optionalDate' inherit the blocker on its declaration +stdlib-fence.ts:83:23 - error SC1090: .throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead) is not supported yet 82 | } - 83 | optionalDate(undefined); - | ^~~~~~~~~~~~ - 84 | function throwDate(): void { - - hint: the declaration of 'optionalDate' did not compile — fix the diagnostic reported there and these sites clear with it - -stdlib-fence.ts:85:9 - error SC1090: throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead) are not supported yet - - 84 | function throwDate(): void { - 85 | throw new Date(0); // the exception cell cannot preserve Date's object kind - | ^~~~~~~~~~~ - 86 | } - -stdlib-fence.ts:91:23 - error SC1090: .throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead) is not supported yet - - 90 | } - 91 | dateGenerator().throw(new Date(0)); + 83 | dateGenerator().throw(new Date(0)); | ^~~~~~~~~~~ - 92 | // Related classes may store one object under different union tags. A + 84 | // Related classes may store one object under different union tags. A -stdlib-fence.ts:102:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet +stdlib-fence.ts:94:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - 101 | function relatedStrict(a: FenceValue, b: FenceValue): boolean { - 102 | return a === b; - | ^~~~~~~ - 103 | } + 93 | function relatedStrict(a: FenceValue, b: FenceValue): boolean { + 94 | return a === b; + | ^~~~~~~ + 95 | } -stdlib-fence.ts:105:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:97:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet - 104 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean { - 105 | return Object.is(a, b); - | ^~~~~~~~~~~~~~~ - 106 | } + 96 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean { + 97 | return Object.is(a, b); + | ^~~~~~~~~~~~~~~ + 98 | } hint: narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms \ No newline at end of file diff --git a/tests/harness/__snapshots__/tuples.ts.txt b/tests/harness/__snapshots__/tuples.ts.txt index 6b43432fe..a6f58710e 100644 --- a/tests/harness/__snapshots__/tuples.ts.txt +++ b/tests/harness/__snapshots__/tuples.ts.txt @@ -40,14 +40,14 @@ tuples.ts:31:13 - error SC1090: '.length' of a computed tuple expression (the ar | ^~~~~~~~~~~ 32 | -tuples.ts:35:13 - error SC1090: '.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet +tuples.ts:35:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet 34 | const recs: { id: number }[] = [{ id: 1 }]; 35 | console.log(recs.join(",")); | ^~~~~~~~~~~~~~ 36 | const tuples: [string, string][] = [["k", "v"]]; -tuples.ts:37:13 - error SC1090: '.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet +tuples.ts:37:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet 36 | const tuples: [string, string][] = [["k", "v"]]; 37 | console.log(tuples.join(";")); diff --git a/tests/harness/__snapshots__/utility-types.ts.txt b/tests/harness/__snapshots__/utility-types.ts.txt index 02595967b..0c6c98489 100644 --- a/tests/harness/__snapshots__/utility-types.ts.txt +++ b/tests/harness/__snapshots__/utility-types.ts.txt @@ -1,17 +1,8 @@ -utility-types.ts:15:7 - error SC2006: values of type '{ [s: symbol]: string; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +utility-types.ts:13:7 - error SC2006: values of type '{ [s: symbol]: string; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') - 14 | // named recursive shapes; differential corpus covers both.) - 15 | const bySymbol: { [s: symbol]: string } = {}; + 12 | // named recursive shapes; differential corpus covers both.) + 13 | const bySymbol: { [s: symbol]: string } = {}; | ^~~~~~~~ - 16 | console.log(bySymbol); - - hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering - -utility-types.ts:18:7 - error SC2006: values of type '{ [k: string]: Date; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') - - 17 | - 18 | const dateBag: { [k: string]: Date } = {}; - | ^~~~~~~ - 19 | const boot = dateBag["boot"]; + 14 | console.log(bySymbol); hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering \ No newline at end of file diff --git a/tests/harness/corpus-costs/5965-date-objects.ts.json b/tests/harness/corpus-costs/5965-date-objects.ts.json new file mode 100644 index 000000000..c59069ff3 --- /dev/null +++ b/tests/harness/corpus-costs/5965-date-objects.ts.json @@ -0,0 +1 @@ +{ "plain": 3455, "san": 5063 } From fa843548a54d0e1ebe0b34a64df2ed4718eadd3f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:36:52 -0500 Subject: [PATCH 10/45] Keep Dates as time values where no use observes their identity --- .../compiler/src/backend/llvm/lib-dispatch.ts | 33 ++- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/date-values.ts | 189 ++++++++++++++++++ .../lowering/expressions/identifiers.ts | 7 + .../src/frontend/lowering/lower-stmts.ts | 12 ++ .../compiler/src/frontend/lowering/lowerer.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 4 + .../compiler/test/date-time-values.test.ts | 100 +++++++++ .../corpus/5966-date-time-values.ts.json | 6 + tests/corpus/5966-date-time-values.ts | 80 ++++++++ .../5966-date-time-values.ts.json | 1 + 11 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/date-values.ts create mode 100644 packages/compiler/test/date-time-values.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json create mode 100644 tests/corpus/5966-date-time-values.ts create mode 100644 tests/harness/corpus-costs/5966-date-time-values.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 753f296e6..293d98a91 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -559,21 +559,25 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll ? "scr_stream_dyn_async_iterator_open" : LIB_FN_SYMS[e.fn]); if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); + // The runtime's Date functions take and answer time values: a Date + // operand passes its time value, read at once from the borrowed object, + // and a Date result is a new Date object. const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) : e.args.map((a) => - borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn) - ? emitBorrowedInput(host, a) - : host.emitExpr(a), + a.type.kind === "date" + ? { name: emitDateTimeArg(host, a), type: F64 } + : borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn) + ? emitBorrowedInput(host, a) + : host.emitExpr(a), ); + // getTime and valueOf answer the time value the argument already is. + if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date") + return { name: args[0]!.name, type: e.type }; // The deep comparisons' number test: computed here, with no runtime call. if (e.fn === "assert.sameValue") return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type }; - // The runtime's Date functions take and answer time values: a Date - // operand passes its time value, and a Date result is a new Date object. - const callArgs = args.map((a) => - a.type.kind === "date" ? { name: emitDateTime(host, a.name), type: F64 } : a, - ); + const callArgs = args; if (e.type.kind === "date") { const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs); host.declare(`declare ptr @scr_date_new(double)`); @@ -584,6 +588,19 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll return emitSymbolLibCall(host, e, sym, callArgs); } +/** The time value of a Date argument. A Date that a runtime Date function + * produces for this argument alone (`new Date(ms).getTime()`, a + * time-value local's read) passes the function's time value and creates + * no Date object. */ +function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string { + // A time-value local's read is its time value itself. + if (date.kind === "libCall" && date.fn === "date.fromTime") + return host.emitExpr(date.args[0]!).name; + if (date.kind === "libCall" && date.fn.startsWith("date.")) + return emitGenericLibCall(host, { ...date, type: F64 }).name; + return emitDateTime(host, emitBorrowedInput(host, date).name); +} + /** A Date object's time value (ScrDate.ms). */ function emitDateTime(host: LlvmEmitterContext, date: string): string { const B = host.B; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..abe43cb5e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -330,6 +330,7 @@ export const LIB_FN_SYMS: Record = { "date.parse": "scr_date_parse", "date.getTime": "scr_date_get_time", "date.valueOf": "scr_date_get_time", + "date.fromTime": "scr_date_get_time", "date.parseGetTime": "scr_date_parse", "date.utc": "scr_date_utc", "date.newLocal": "scr_date_new_local", diff --git a/packages/compiler/src/frontend/lowering/date-values.ts b/packages/compiler/src/frontend/lowering/date-values.ts new file mode 100644 index 000000000..f40db4e6d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/date-values.ts @@ -0,0 +1,189 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Date methods whose receiver is only read for its time value: a Date + * local used as their receiver can store the time value instead of a Date + * object (builtins/dates.ts lowers them). */ +const TIME_VALUE_METHODS = new Set([ + "getTime", + "valueOf", + "toISOString", + "toJSON", + "toString", + "toDateString", + "toTimeString", + "toUTCString", + "getFullYear", + "getUTCFullYear", + "getMonth", + "getUTCMonth", + "getDate", + "getUTCDate", + "getDay", + "getUTCDay", + "getHours", + "getUTCHours", + "getMinutes", + "getUTCMinutes", + "getSeconds", + "getUTCSeconds", + "getMilliseconds", + "getUTCMilliseconds", + "getTimezoneOffset", +]); + +const RELATIONAL = new Set([ + ts.SyntaxKind.LessThanToken, + ts.SyntaxKind.LessThanEqualsToken, + ts.SyntaxKind.GreaterThanToken, + ts.SyntaxKind.GreaterThanEqualsToken, +]); + +/** True when a local `const` (or a `let` that is never assigned again) + * holding a Date can keep the Date's time value instead of a Date object: + * no use observes the Date's identity. Every use reads the time value as + * a Date method receiver, a String() or template operand, a relational or + * string `+` operand, or `new Date(d)`'s argument, inside the declaring + * function, or inside the block or loop that scopes a module-level + * binding (no closure captures it). Top-level module bindings keep the + * object. Other uses box the time value into a new Date at the use. */ +export function isTimeValueDateDeclaration( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): boolean { + const known = lowerer.timeValueDateDecisions.get(decl); + if (known !== undefined) return known; + const answer = decideTimeValueDate(lowerer, decl); + lowerer.timeValueDateDecisions.set(decl, answer); + return answer; +} + +function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return false; + const sf = decl.getSourceFile(); + if (sf.isDeclarationFile || /\.[cm]?jsx?$/.test(sf.fileName)) return false; + const list = decl.parent; + if (!ts.isVariableDeclarationList(list)) return false; + const isConst = (list.flags & ts.NodeFlags.Const) !== 0; + const isLet = (list.flags & ts.NodeFlags.Let) !== 0; + if (!isConst && !isLet) return false; + if (!ts.isVariableStatement(list.parent) && !ts.isForStatement(list.parent)) return false; + const fn = enclosingFunction(decl); + // The uses to check: the function body, or for a binding inside a block + // or loop at module level (a local of the module's initializer, not a + // module binding) the block or loop that scopes it. + let scope: ts.Node | undefined; + if (fn !== null) scope = fn.body; + else if (!inClassStaticBlock(decl)) scope = moduleBlockScope(decl); + if (scope === undefined) return false; + // The lib Date interface by its symbol first: mapping every candidate + // binding's type would intern its unions before the lowering does. + const typeSymbol = lowerer.typeOf(decl.name).getSymbol(); + if ( + typeSymbol === undefined || + ts.symbolName(typeSymbol) !== "Date" || + !typeSymbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) + ) + return false; + if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "date") return false; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if (!symbol) return false; + let ok = true; + const visit = (node: ts.Node): void => { + if (!ok) return; + if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) { + if (lowerer.checker.getSymbolAtLocation(node) === symbol) { + if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false; + } + return; + } + ts.forEachChild(node, visit); + }; + visit(scope); + return ok; +} + +/** The block or for statement that scopes a module-level binding which is + * not a module binding: a declaration in a block or loop body, or in a + * for statement's initializer. Undefined for a top-level declaration. */ +function moduleBlockScope(decl: ts.VariableDeclaration): ts.Node | undefined { + const list = decl.parent; + if (!list || !ts.isVariableDeclarationList(list)) return undefined; + if (ts.isForStatement(list.parent)) return list.parent; + const statement = list.parent; + if (!ts.isVariableStatement(statement) || ts.isSourceFile(statement.parent)) return undefined; + for (let cur: ts.Node | undefined = statement.parent; cur; cur = cur.parent) { + if (ts.isBlock(cur) || ts.isCaseBlock(cur)) return cur; + if (ts.isSourceFile(cur)) return undefined; + } + return undefined; +} + +function inClassStaticBlock(node: ts.Node): boolean { + for (let cur = node.parent; cur; cur = cur.parent) { + if (ts.isClassStaticBlockDeclaration(cur)) return true; + if (ts.isSourceFile(cur)) return false; + } + return false; +} + +function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null { + for (let cur = node.parent; cur; cur = cur.parent) { + if ( + ts.isFunctionDeclaration(cur) || + ts.isFunctionExpression(cur) || + ts.isArrowFunction(cur) || + ts.isMethodDeclaration(cur) || + ts.isConstructorDeclaration(cur) || + ts.isGetAccessorDeclaration(cur) || + ts.isSetAccessorDeclaration(cur) + ) + return cur; + if (ts.isClassStaticBlockDeclaration(cur) || ts.isSourceFile(cur)) return null; + } + return null; +} + +/** A use of the binding that reads only its time value. */ +function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean { + let node: ts.Node = id; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isPropertyAccessExpression(parent) && parent.expression === node) { + const call = parent.parent; + return ( + !parent.questionDotToken && + ts.isCallExpression(call) && + call.expression === parent && + !call.questionDotToken && + TIME_VALUE_METHODS.has(parent.name.text) && + lowerer.isStdlibMember(parent) + ); + } + if (ts.isTemplateSpan(parent)) return parent.expression === node; + if (ts.isCallExpression(parent)) { + return ( + parent.arguments.nodes.length === 1 && + parent.arguments.nodes[0] === node && + ts.isIdentifier(parent.expression) && + lowerer.isStdlibGlobal(parent.expression, "String") + ); + } + if (ts.isNewExpression(parent)) { + return ( + parent.arguments?.nodes.length === 1 && + parent.arguments.nodes[0] === node && + ts.isIdentifier(parent.expression) && + lowerer.isStdlibGlobal(parent.expression, "Date") + ); + } + if (ts.isBinaryExpression(parent)) { + const other = parent.left === node ? parent.right : parent.left; + const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind; + if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date"; + if (parent.operatorToken.kind === ts.SyntaxKind.PlusToken) return otherKind === "string"; + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ff7848930..2c82a3c64 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -8,6 +8,7 @@ import { type Lowerer, PoisonError } from "../lowerer.js"; import { BOOL, BYTES_ELEMENT_NAME, + DATE_T, DYN, F64, type IrExpr, @@ -170,6 +171,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // when the checker types this USE as a single arm, maybeNarrow // bridges the tagged representation with a unionNarrow. const local = lowerer.resolveLocal(expr); + // A Date local holding its time value reads as that Date: only uses + // that read the time value reach here (date-values.ts). + if (local && lowerer.timeValueDateLocals.has(local)) { + const time: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc }; + return { kind: "libCall", fn: "date.fromTime", args: [time], type: DATE_T, loc }; + } if (local) { // A switch can dispatch past a lexical declaration. Keep this // previously refused codec form out until switch-scope TDZ boxes diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 750057bf9..f4d6564ba 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -28,6 +28,7 @@ import * as ts from "../ts7/adapter.js"; import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; +import { isTimeValueDateDeclaration } from "./date-values.js"; import { bindingSource } from "../binding-source.js"; import { lowerConstructorReturn } from "./constructor-returns.js"; import type { Lowerer } from "./lowerer.js"; @@ -63,6 +64,7 @@ import { BOOL, BYTES_U8, CAUGHT, + DATE_T, DYN, F64, type IrExpr, @@ -5234,6 +5236,16 @@ export function lowerVarDecl( if (alias) return alias; } + // A Date local whose identity no use observes stores its time value. + if (isTimeValueDateDeclaration(lowerer, decl)) { + const loc = locOf(decl); + const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T); + const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet); + lowerer.timeValueDateLocals.add(local); + const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc }; + return { kind: "varDecl", localId: local.id, init, loc }; + } + // A NULLISH generic binding (`const i: I = null as any` — the // declared type has no mapping, the value is provably null/undefined // forever): no storage exists; reads know the value (member reads and diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..637dfcaa8 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2331,6 +2331,10 @@ export class Lowerer { readonly runtimeOptionalFields = new Set(); /** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */ readonly dateJsonBindings = new Set(); + /** isTimeValueDateDeclaration's answers, and the locals that hold a + * Date's time value instead of a Date object (date-values.ts). */ + readonly timeValueDateDecisions = new Map(); + readonly timeValueDateLocals = new Set(); /** Record bindings whose flow class reaches storage outside the class: * they keep the declared layout, so an initializer from unchecked reads * converts once instead of the binding adopting the widened variant. */ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..b89ca61c4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4738,6 +4738,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, + /** A Date object for a time value a local holds (date-values.ts): a + * lib-call argument passes the time value itself, and any other use + * creates the Date. */ + "date.fromTime": { argTypes: [F64], result: DATE_T }, /** Date.prototype.toString / toDateString / toTimeString and String(date): * V8's local-time formats with the host zone's long name, "Invalid Date" * for an invalid date. Owned (+1) string; never throws. */ diff --git a/packages/compiler/test/date-time-values.test.ts b/packages/compiler/test/date-time-values.test.ts new file mode 100644 index 000000000..4e90ef3e6 --- /dev/null +++ b/packages/compiler/test/date-time-values.test.ts @@ -0,0 +1,100 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lowerSource(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-date-time-values-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The IR type of each local named `name` in function `fn`. */ +function localKinds(mod: IrModule, fn: string, name: string): string[] { + const f = mod.functions.find((candidate) => candidate.name === fn); + if (!f) throw new Error(`no function ${fn}`); + return f.locals.filter((local) => local.name === name).map((local) => local.type.kind); +} + +test("a Date local read only for its time value stores the time value", async () => { + const mod = await lowerSource(` +function format(ms: number): string { + const d = new Date(ms); + const later = new Date(ms + 1); + return d.toISOString() + String(d) + \`\${later}\` + (d < later) + d.getUTCHours() + new Date(d).getTime(); +} +console.log(format(0)); +`); + expect(localKinds(mod, "format", "d")).toEqual(["f64"]); + expect(localKinds(mod, "format", "later")).toEqual(["f64"]); + // Reading the time value creates no Date object. + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const body = /^define internal [^\n]*@sc_f_format\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).not.toContain("@scr_date_new("); +}); + +test("a Date local whose identity a use can observe keeps the Date object", async () => { + const mod = await lowerSource(` +const keep: Date[] = []; +function uses(ms: number): boolean { + const compared = new Date(ms); + const stored = new Date(ms); + const passed = new Date(ms); + const captured = new Date(ms); + const returned = new Date(ms); + let reassigned = new Date(ms); + reassigned = new Date(ms + 1); + keep.push(stored); + const read = (): number => captured.getTime(); + console.log(String(passed), read(), returned.getTime(), reassigned.getTime()); + return compared === compared && pass(passed) === passed && back(returned) === returned; +} +function pass(d: Date): Date { + return d; +} +function back(d: Date): Date { + return d; +} +const top = new Date(0); +console.log(uses(0), top.getTime()); +`); + for (const name of ["compared", "stored", "passed", "captured", "returned", "reassigned"]) { + expect(localKinds(mod, "uses", name), name).toEqual(["date"]); + } +}); + +test("a Date in a block or loop at module level stores the time value unless a use observes it", async () => { + const mod = await lowerSource(` +const seen: Date[] = []; +let total = 0; +for (let i = 0; i < 3; i++) { + const read = new Date(i * 1000); + const kept = new Date(i * 1000); + seen.push(kept); + total += read.getUTCSeconds() + read.toISOString().length; +} +{ + const block = new Date(5); + const captured = new Date(6); + const later = (): number => captured.getTime(); + total += block.getTime() + later(); +} +const top = new Date(7); +console.log(total, seen.length, top.getTime()); +`); + expect(localKinds(mod, "%init.0", "read")).toEqual(["f64"]); + expect(localKinds(mod, "%init.0", "block")).toEqual(["f64"]); + expect(localKinds(mod, "%init.0", "kept")).toEqual(["date"]); + expect(localKinds(mod, "%init.0", "captured")).toEqual(["date"]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json new file mode 100644 index 000000000..e6155bcc4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5966-date-time-values.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5966-date-time-values.ts b/tests/corpus/5966-date-time-values.ts new file mode 100644 index 000000000..342b4662c --- /dev/null +++ b/tests/corpus/5966-date-time-values.ts @@ -0,0 +1,80 @@ +// Date locals that are only read for their time value (method receivers, +// String() and template operands, relational and string `+` operands, +// `new Date(d)`) print and compare as Dates; locals whose identity a use +// can observe keep it. +export {}; +process.env.TZ = "America/New_York"; + +function describe(ms: number): string { + const d = new Date(ms); + const later = new Date(ms + 3_600_000); + const copy = new Date(d); + return [ + d.toISOString(), + d.toUTCString(), + String(d), + `${later}`, + "at " + d, + d < later, + later >= d, + d.getHours(), + d.getUTCDay(), + d.getTimezoneOffset(), + copy.getTime() === d.getTime(), + d.toJSON(), + ].join(" | "); +} +console.log(describe(Date.UTC(2026, 2, 8, 6, 30))); +console.log(describe(Date.UTC(2026, 10, 1, 5, 30))); +function invalid(): string { + const d = new Date(NaN); + return [String(d), d.getTime(), String(d.toJSON()), d < new Date(0), `${d}`].join(" | "); +} +console.log(invalid()); + +function sum(days: number): number { + let total = 0; + for (let i = 0; i < days; i++) { + const day = new Date(Date.UTC(2026, 0, 1) + i * 86_400_000); + total += day.getUTCDate() + day.getUTCMonth() + day.toISOString().length; + } + return total; +} +console.log(sum(400)); + +// Identity-observing uses keep the Date object. +const kept: Date[] = []; +function identities(ms: number): string { + const a = new Date(ms); + const b = a; + const stored = new Date(ms); + kept.push(stored); + const captured = new Date(ms); + const read = (): number => captured.getTime(); + const passed = new Date(ms); + return [a === b, kept.includes(stored), kept[0] === stored, read(), same(passed, passed)].join(","); +} +function same(x: Date, y: Date): boolean { + return x === y; +} +console.log(identities(5)); +const held: unknown = kept[0]; +console.log(held instanceof Date, (held as Date) === kept[0]); + +// Dates in blocks and loops at module level, read for their time value or +// kept, stored and captured. +const moduleKept: Date[] = []; +let moduleTotal = ""; +for (let i = 0; i < 3; i++) { + const read = new Date(Date.UTC(2026, 2, 8 + i, 6, 30)); + const kept = new Date(read.getTime()); + moduleKept.push(kept); + moduleTotal += `${read.toISOString()} ${read.getHours()} ${String(read)}\n`; +} +{ + const block = new Date(Date.UTC(2026, 10, 1, 5, 59)); + const captured = new Date(block); + const later = (): string => captured.toISOString(); + moduleTotal += `${block.getTimezoneOffset()} ${later()} ${captured === moduleKept[0]}`; +} +console.log(moduleTotal, moduleKept.map((d) => d.getUTCDate()).join(",")); diff --git a/tests/harness/corpus-costs/5966-date-time-values.ts.json b/tests/harness/corpus-costs/5966-date-time-values.ts.json new file mode 100644 index 000000000..a3a32b65e --- /dev/null +++ b/tests/harness/corpus-costs/5966-date-time-values.ts.json @@ -0,0 +1 @@ +{ "plain": 3096, "san": 3319 } From ac40773362d71f04c5564a7ba85ba4220ce90a3f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:54:19 -0500 Subject: [PATCH 11/45] Excuse date.fromTime in the attestation parity test date.fromTime makes a Date from the time value a Date local holds and reads no ambient state. The local's initializer is the Date expression the fences witness. --- tests/harness/surface-manifest.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 2ff3c71c2..58958c102 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -558,6 +558,10 @@ describe("determinism attestation ↔ fence parity", () => { // witnessed Date method; assigning it to getTime would also deny // valueOf/toISOString/etc. under a getTime-only fence. "date.checkedValue", + // Makes a Date from the time value a Date local holds + // (date-values.ts) without reading ambient state. The local's + // initializer is the Date expression the fences witness. + "date.fromTime", ]); const missing = Object.keys(LIB_FN_SIGS).filter( (fn) => From b9275c2693597c3e79287fcbedc80f5d49814d5f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:05:19 -0500 Subject: [PATCH 12/45] buffer: fromArrayBuffer ranges, the deprecated constructor, Buffer as a value --- .../limitations/buffer-construction.mdx | 5 + .../ambient/scriptc-node-fallback.d.ts | 8 + .../compiler/src/backend/llvm/lib-shared.ts | 9 + .../src/frontend/lowering/containers/bytes.ts | 259 ++++++++++++++---- .../lowering/expressions/element-access.ts | 3 + .../lowering/expressions/identifiers.ts | 3 + .../lowering/expressions/property-access.ts | 45 +++ .../src/frontend/lowering/lower-calls.ts | 9 +- .../src/frontend/lowering/lower-classes.ts | 45 +-- .../src/frontend/lowering/lower-inspect.ts | 20 +- .../compiler/src/frontend/lowering/lowerer.ts | 5 + .../src/frontend/lowering/surfaces.ts | 3 + packages/compiler/src/frontend/program.ts | 5 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 9 + packages/compiler/src/ir/int32-slots.ts | 2 + packages/compiler/src/ir/runtime-features.ts | 21 +- .../7527-buffer-from-array-buffer.ts.json | 6 + .../7528-buffer-from-array-buffer.cjs.json | 6 + .../corpus/7529-buffer-constructor.cjs.json | 6 + .../corpus/7530-buffer-constructor.ts.json | 6 + .../main.cjs.json | 18 ++ .../main.cjs.json | 18 ++ .../tests/corpus/7533-buffer-value.cjs.json | 6 + .../7534-buffer-prototype-members.cjs.json | 6 + .../7535-inspect-function-properties.cjs.json | 6 + packages/runtime/src/scr_async.h | 4 + packages/runtime/src/scr_async_dyn.c | 16 ++ packages/runtime/src/scr_builtin_values.c | 214 +++++++++++++++ packages/runtime/src/scr_bytes.c | 20 +- packages/runtime/src/scr_bytes.h | 27 ++ packages/runtime/src/scr_bytes_dyn.c | 203 ++++++++++++++ packages/runtime/src/scr_bytes_io.c | 66 ++++- packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_inspect.c | 40 ++- packages/runtime/src/scr_json.c | 22 +- tests/corpus/2570-buffer-arg-validation.cjs | 3 +- tests/corpus/7527-buffer-from-array-buffer.ts | 33 +++ .../corpus/7528-buffer-from-array-buffer.cjs | 31 +++ tests/corpus/7529-buffer-constructor.cjs | 80 ++++++ tests/corpus/7530-buffer-constructor.ts | 42 +++ .../main.cjs | 36 +++ .../node_modules/legacy-buf/index.d.ts | 3 + .../node_modules/legacy-buf/index.js | 15 + .../node_modules/legacy-buf/package.json | 5 + .../main.cjs | 10 + .../node_modules/legacy-buf/index.d.ts | 3 + .../node_modules/legacy-buf/index.js | 15 + .../node_modules/legacy-buf/package.json | 5 + tests/corpus/7533-buffer-value.cjs | 101 +++++++ .../corpus/7534-buffer-prototype-members.cjs | 29 ++ .../7535-inspect-function-properties.cjs | 29 ++ .../7527-buffer-from-array-buffer.ts.json | 1 + .../7528-buffer-from-array-buffer.cjs.json | 1 + .../7529-buffer-constructor.cjs.json | 1 + .../7530-buffer-constructor.ts.json | 1 + .../main.cjs.json | 1 + .../main.cjs.json | 1 + .../corpus-costs/7533-buffer-value.cjs.json | 1 + .../7534-buffer-prototype-members.cjs.json | 1 + .../7535-inspect-function-properties.cjs.json | 1 + tests/harness/differential.test.ts | 6 +- tests/harness/errors.test.ts | 49 ++++ 63 files changed, 1535 insertions(+), 117 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json create mode 100644 tests/corpus/7527-buffer-from-array-buffer.ts create mode 100644 tests/corpus/7528-buffer-from-array-buffer.cjs create mode 100644 tests/corpus/7529-buffer-constructor.cjs create mode 100644 tests/corpus/7530-buffer-constructor.ts create mode 100644 tests/corpus/7531-buffer-constructor-dependency/main.cjs create mode 100644 tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts create mode 100644 tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js create mode 100644 tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/main.cjs create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json create mode 100644 tests/corpus/7533-buffer-value.cjs create mode 100644 tests/corpus/7534-buffer-prototype-members.cjs create mode 100644 tests/corpus/7535-inspect-function-properties.cjs create mode 100644 tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json create mode 100644 tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json create mode 100644 tests/harness/corpus-costs/7529-buffer-constructor.cjs.json create mode 100644 tests/harness/corpus-costs/7530-buffer-constructor.ts.json create mode 100644 tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json create mode 100644 tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json create mode 100644 tests/harness/corpus-costs/7533-buffer-value.cjs.json create mode 100644 tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json create mode 100644 tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json diff --git a/docs/content/limitations/buffer-construction.mdx b/docs/content/limitations/buffer-construction.mdx index aca0c74ca..c88e01336 100644 --- a/docs/content/limitations/buffer-construction.mdx +++ b/docs/content/limitations/buffer-construction.mdx @@ -1,3 +1,8 @@ `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. +`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks. Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error. +The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does. +In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`. +Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error. +`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b195c3fbb..e26faf956 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -917,6 +917,14 @@ interface Buffer< readBigUint64LE(offset?: number): bigint; } interface BufferConstructor { + /** @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. */ + new (str: string, encoding?: BufferEncoding): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.alloc()` instead. */ + new (size: number): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. */ + new (array: ArrayLike): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. */ + new (arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer; from(data: string, encoding?: BufferEncoding): Buffer; /* The ArrayBuffer form is the VIEW construction: x.buffer with an * optional byte offset/length shares x's storage (the subarray rule). */ diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7dd4370c5..b92047e43 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -454,6 +454,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", "buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled", + "buffer.poolFromDynRange": "scr_buffer_from_dyn_range_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", @@ -479,6 +480,12 @@ export const LIB_FN_SYMS: Record = { "bytes.equalsChk": "scr_bytes_equals_chk", "bytes.compareChk": "scr_bytes_compare_chk", "buffer.newStringFail": "scr_buffer_new_string_fail", + "buffer.deprecation": "scr_buffer_deprecation", + "buffer.deprecationCount": "scr_buffer_deprecation_count", + "buffer.constructor": "scr_buffer_constructor_value", + "buffer.installValue": "scr_buffer_install_value", + "buffer.staticValue": "scr_buffer_static_value", + "buffer.prototypeMember": "scr_buffer_prototype_member", "fs.toUnixTimestamp": "scr_fs_to_unix_timestamp", "fs.existsChk": "scr_fs_exists_async", "fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk", @@ -1661,6 +1668,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "url.legacyParse", "url.legacyFormat", "url.legacyResolve", + // The deprecated Buffer constructor reports DEP0005 on the next tick. + "buffer.deprecation", // The wrappers schedule warnings and callbacks on the tick queue. "util.deprecate", "util.callbackify", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 995cb177a..82c6f3052 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -15,6 +15,7 @@ import { type IrLibFn, type IrLocal, type IrParam, + type IrFunction, type IrStmt, type IrType, STRING, @@ -30,7 +31,8 @@ import { import { isJsSourceFile, locOf } from "../../program.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import type { Lowerer } from "../lowerer.js"; -import { newFnCtx, own } from "../lowerer.js"; +import { ENTRY_NAME, newFnCtx, own } from "../lowerer.js"; +import { everyStmt } from "../../../ir/traverse.js"; import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js"; import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; @@ -1807,7 +1809,12 @@ function knownBufferProducer( * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an * ArrayBuffer takes the second argument as its byte offset; other sources * ignore it. */ -function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { +function dynBufferFromParts( + source: IrExpr, + encoding: IrExpr, + loc: SrcLoc, + length?: IrExpr, +): IrExpr { const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; const isString = (x: IrExpr): IrExpr => ({ kind: "dynTest", @@ -1848,20 +1855,48 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx type: BYTES_U8, loc, }, - // An ArrayBuffer takes the second argument as its byte offset - // (scr_buffer_from_dyn_offset_pooled); other values convert with utf8. - else_: { - kind: "libCall", - fn: "buffer.poolFromDynOffset", - args: [source, encoding], - type: BYTES_U8, - loc, - }, + // An ArrayBuffer takes the second argument as its byte offset and the + // third as its length (scr_buffer_from_dyn_range_pooled); other values + // convert with utf8. + else_: length + ? { + kind: "libCall", + fn: "buffer.poolFromDynRange", + args: [source, encoding, length], + type: BYTES_U8, + loc, + } + : { + kind: "libCall", + fn: "buffer.poolFromDynOffset", + args: [source, encoding], + type: BYTES_U8, + loc, + }, type: BYTES_U8, loc, }; } +/** Buffer.from(arrayBuffer, byteOffset[, length]): each argument in order + * as a checked-dynamic value, read by the runtime as Node's + * fromArrayBuffer reads them. */ +function bufferFromArrayBufferRange( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, + first?: IrExpr, +): IrExpr { + const operands = [0, 1, 2].map((i) => + i === 0 && first + ? first + : args[i] + ? lowerer.lowerExprExpecting(args[i]!, DYN) + : dynUndefinedExpr(loc), + ); + return { kind: "libCall", fn: "buffer.poolFromDynRange", args: operands, type: BYTES_U8, loc }; +} + /** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in * source order, and builds the result from their references. */ function withDynBufferArguments( @@ -1909,52 +1944,81 @@ function dynBufferFromRuntimeEncoding( ); } -/** The deprecated `new Buffer(arg[, encoding])` in a dependency's - * JavaScript, through the global or an alias of it (buffer-crc32 and yazl - * keep it as the fallback for runtimes without Buffer.alloc/Buffer.from). - * Node never emits DEP0005 for calls from node_modules, so the construction - * is exactly Node's: a number allocates zero-filled bytes (and throws - * ERR_INVALID_ARG_TYPE beside a string second argument), anything else - * converts as Buffer.from does. Null elsewhere: other callers owe the - * deprecation warning. */ -export function lowerNodeModulesBufferConstruct( +/** The deprecated `Buffer(arg[, encodingOrOffset[, length]])`, called or + * constructed through the global or an alias of it, as Node's Buffer + * function runs it: the arguments evaluate in order, DEP0005 is reported + * on the next tick (once, and never for a call made from node_modules, + * which only counts toward Node's 10000-call check), then a number + * allocates zero-filled bytes (and throws ERR_INVALID_ARG_TYPE beside a + * string second argument) and anything else converts as Buffer.from does. + * Null for spread arguments and more than three. */ +export function lowerBufferConstruct( lowerer: Lowerer, - expr: ts.NewExpression, + expr: ts.NewExpression | ts.CallExpression, ): IrExpr | null { - const sf = expr.getSourceFile(); - if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null; + if (ts.isCallExpression(expr) && expr.questionDotToken) return null; const args = expr.arguments?.nodes ?? []; - if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + if (args.length > 3 || args.some(ts.isSpreadElement)) return null; if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null; const loc = locOf(expr); - const value = withDynBufferArguments( - lowerer, - lowerer.lowerExpr(args[0]!), - args[1], - loc, - (source, encoding) => ({ + const locals = [0, 1, 2].slice(0, Math.max(args.length, 2)).map((i) => { + const local = lowerer.declareHiddenLocal("%bufferArgument", DYN); + const node = args[i]; + const init = node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN); + return { local, init }; + }); + const [source, second, third] = locals.map(({ local }) => varRef(local.id, DYN, loc)) as [ + IrExpr, + IrExpr, + IrExpr | undefined, + ]; + const inNodeModules = /[\\/]node_modules[\\/]/.test(expr.getSourceFile().fileName); + const value: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, + then: { kind: "ternary", - cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, - then: { - kind: "ternary", - cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc }, - then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, - else_: { - kind: "libCall", - fn: "buffer.alloc", - args: [{ kind: "dynCheck", value: source, type: F64, loc }], - type: BYTES_U8, - loc, - }, + cond: { kind: "dynTest", test: "string", value: second, type: BOOL, loc }, + then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, + else_: { + kind: "libCall", + fn: "buffer.alloc", + args: [{ kind: "dynCheck", value: source, type: F64, loc }], type: BYTES_U8, loc, }, - else_: dynBufferFromParts(source, encoding, loc), type: BYTES_U8, loc, - }), - ); - return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }; + }, + else_: dynBufferFromParts(source, second, loc, third), + type: BYTES_U8, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + ...locals.map(({ local, init }): IrStmt => ({ + kind: "varDecl", + localId: local.id, + init, + loc, + })), + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: inNodeModules ? "buffer.deprecationCount" : "buffer.deprecation", + args: [], + type: VOID, + loc, + }, + loc, + }, + ], + result: { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }, + type: BYTES_U8, + loc, + }; } /** Buffer.from(array or bytes, extra): Node copies an array-like source and @@ -2018,7 +2082,16 @@ function lowerBufferStaticValue( lowerer.typeOf(args[0]!).getSymbol(), ) ) { - return lowerArrayBufferView(lowerer, args, "u8", loc); + // A byte offset or length follows Node's fromArrayBuffer (its + // conversions and ERR_BUFFER_OUT_OF_BOUNDS), not the typed-array view. + if (args.length === 1) return lowerArrayBufferView(lowerer, args, "u8", loc); + return bufferFromArrayBufferRange(lowerer, args, loc); + } + // Buffer.from(value, byteOffset, length) over an untyped value: an + // ArrayBuffer's range, or the two-argument conversion of other values. + if (args.length === 3 && !args.some(ts.isSpreadElement)) { + const first = lowerer.lowerExpr(args[0]!); + if (first.type.kind === "dyn") return bufferFromArrayBufferRange(lowerer, args, loc, first); } // Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing // x's storage (Node shares the ArrayBuffer; length is in BYTES). @@ -2445,11 +2518,101 @@ function lowerBufferStaticValue( const s = lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", fn: "buffer.isEncoding", args: [s], type: BOOL, loc }; } + // Any other single argument: Node answers false for non-strings. + if (args.length === 1 && !ts.isSpreadElement(args[0]!)) { + const value = lowerer.declareHiddenLocal("%bufferEncoding", DYN); + const ref = varRef(value.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: value.id, + init: lowerer.lowerExprExpecting(args[0]!, DYN), + loc, + }, + ], + result: { + kind: "logical", + op: "&&", + left: { kind: "dynTest", test: "string", value: ref, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "buffer.isEncoding", + args: [{ kind: "dynCheck", value: ref, type: STRING, loc }], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } lowerer.noLowering( "Buffer.isEncoding with this argument shape", call, - "one string-typed argument is the lowered form", + "one argument is the lowered form", ); } return null; // of, copyBytesFrom, ... → the SC2020 member fence } + +/** `Buffer.prototype.` or `Buffer.prototype[key]` read in JavaScript + * (smart-buffer's `typeof Buffer.prototype[method]` probe): the member as + * scr_buffer_prototype_member answers it. Null for other expressions. */ +export function lowerBufferPrototypeMember( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression | ts.ElementAccessExpression, +): IrExpr | null { + if (expr.questionDotToken || !isJsSourceFile(expr.getSourceFile())) return null; + const receiver = expr.expression; + if ( + !ts.isPropertyAccessExpression(receiver) || + receiver.questionDotToken || + receiver.name.text !== "prototype" || + stdlibGlobalNameOf(lowerer, receiver.expression) !== "Buffer" + ) + return null; + const loc = locOf(expr); + const key: IrExpr = ts.isPropertyAccessExpression(expr) + ? { + kind: "dynFrom", + value: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + } + : lowerer.lowerExprExpecting(expr.argumentExpression, DYN); + return { kind: "libCall", fn: "buffer.prototypeMember", args: [key], type: DYN, loc }; +} + +const BUFFER_VALUE_FNS: ReadonlySet = new Set([ + "buffer.constructor", + "buffer.staticValue", + "buffer.prototypeMember", +]); + +/** A program that reads the Buffer function's value installs it in %main, + * so a Buffer's `.constructor` answers the same function. */ +export function installBufferValue(functions: IrFunction[]): void { + let found = false; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "libCall" && BUFFER_VALUE_FNS.has(e.fn)) found = true; + return !found; + }, + stmt: (): boolean => !found, + }; + for (const fn of functions) { + for (const s of fn.body) everyStmt(s, visitor); + if (found) break; + } + const main = functions.find((fn) => fn.name === ENTRY_NAME); + if (!found || !main) return; + main.body.unshift({ + kind: "exprStmt", + expr: { kind: "libCall", fn: "buffer.installValue", args: [], type: VOID, loc: main.loc }, + loc: main.loc, + }); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0ea94ec7a..24de38630 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -71,6 +71,7 @@ import { isClassCallback } from "../class-callbacks.js"; import { isKeyedFieldAccess } from "./keyed-field-targets.js"; import { lowerFieldCompoundValue } from "./updates.js"; import { readsHeldEvolvingArray } from "../lower-stmts.js"; +import { lowerBufferPrototypeMember } from "../containers/bytes.js"; /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS * string-key element access (`a["length"]`) and string indexing (`s[0]` @@ -104,6 +105,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre const en = lowerEnumAccess(lowerer, expr); if (en) return en; } + const bufferMember = lowerBufferPrototypeMember(lowerer, expr); + if (bufferMember) return bufferMember; const cachedModule = lowerRequireCacheElement(lowerer, expr); if (cachedModule) return cachedModule; const namespaceElement = lowerModuleNamespaceElement(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ef0d8a2d1..1570ea709 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -744,6 +744,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { + // JavaScript reads the Buffer function's value (builtinGlobalValue). + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; } if (lowerer.isStdlibGlobal(expr, "fetch")) { diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7b12297a8..607ad221b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -153,6 +153,7 @@ import { lowerStringCodesValue, } from "../lower-builtin-values.js"; import { lowerArrayFromValue } from "../containers/array-construction.js"; +import { lowerBufferPrototypeMember } from "../containers/bytes.js"; import { lowerNetAddressStaticRead } from "../builtins/net-address.js"; import { STATS_TYPE_TESTS } from "../builtins/filesystem.js"; import { arrayValueRead, arrayValueType } from "../array-values.js"; @@ -310,6 +311,10 @@ export function lowerPropertyAccessExpr( // Cloudflare probe) folds like `navigator.userAgent`. const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; + { + const member = lowerBufferPrototypeMember(lowerer, expr); + if (member) return member; + } // A dgram socket's method read as a value in JavaScript (`emit: // socket.emit.bind(socket)`): the socket crosses as its checked-dynamic // handle, whose methods are function values that dispatch on their @@ -1769,6 +1774,28 @@ export function lowerIntrinsicProperty( return lowerNumberParserValue(lowerer, numberMember, locOf(expr)); if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize") return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) }; + { + const member = lowerer.stdlibGlobalMember(expr, "Buffer"); + if (member === "name") + return { kind: "strLit", value: "Buffer", type: STRING, loc: locOf(expr) }; + if (member === "length") return numLit(3, locOf(expr)); + // JavaScript reads the statics from the Buffer function's value, so + // each has one identity and Node's name and length. + if ( + member !== null && + BUFFER_VALUE_STATICS.has(member) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) { + const loc = locOf(expr); + return { + kind: "libCall", + fn: "buffer.staticValue", + args: [{ kind: "strLit", value: member, type: STRING, loc }], + type: DYN, + loc, + }; + } + } const netAddressStatic = lowerNetAddressStaticRead(lowerer, expr); if (netAddressStatic) return netAddressStatic; if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") @@ -4173,6 +4200,8 @@ export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr { * null when it stays an identity token (JavaScript) or fenced. */ export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null { if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc); + if (js && canonical === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) { return { kind: "libCall", @@ -4300,6 +4329,22 @@ function fenceBuiltinConstructorStaticRead( return null; } +/** The Buffer statics the Buffer function's value holds + * (scr_buffer_constructor_value). */ +const BUFFER_VALUE_STATICS: ReadonlySet = new Set([ + "from", + "copyBytesFrom", + "of", + "alloc", + "allocUnsafe", + "allocUnsafeSlow", + "isBuffer", + "compare", + "isEncoding", + "concat", + "byteLength", +]); + /** Object.prototype members a plain record inherits, read as values. */ const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet = new Set([ "constructor", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 82f5a27c1..03d19f0de 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -119,7 +119,7 @@ import { } from "./containers/indexed-objects.js"; import { lowerTupleReadMethodCall } from "./containers/array-methods.js"; import { tryFuseIndexRead } from "./array-values.js"; -import { lowerBytesStaticCall } from "./containers/bytes.js"; +import { lowerBufferConstruct, lowerBytesStaticCall } from "./containers/bytes.js"; import { lowerRegexMethodCall, lowerStringIndexCall, @@ -335,6 +335,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // The deprecated Buffer(...) call (containers/bytes.ts). The checker + // types it `any`, so the bytes cross as a checked-dynamic value, which + // keeps the Buffer brand for inspection. + { + const buffer = lowerBufferConstruct(lowerer, expr); + if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc }; + } // A package shim's runtime intrinsic (a bodiless `declare function` // named scriptcUnixDgram* in a shipped shim): a direct runtime call. if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 29afbb4fe..64fd1968d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -46,7 +46,6 @@ import { DATE_T, DYN, F64, - bytesOf, type IrClassDef, type IrExpr, type IrFunction, @@ -115,8 +114,7 @@ import { isCjsRequireGlobal } from "./lower-node-module.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js"; -import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; import { @@ -9776,9 +9774,9 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); - // A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts). + // The deprecated `new Buffer(...)` (containers/bytes.ts). { - const buffer = lowerNodeModulesBufferConstruct(lowerer, expr); + const buffer = lowerBufferConstruct(lowerer, expr); if (buffer) return buffer; } // new Function(...): Node's EvalError under @@ -10855,43 +10853,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // families carry pointed hints: each states WHY no honest static // lowering exists (or what to use instead). if (lowerer.isStdlibSymbol(symbol ?? undefined)) { - // The deprecated `new Buffer(string, encoding?)` ctor's string arm - // with a NON-STRING first argument and a string second: Node - // throws ERR_INVALID_ARG_TYPE synchronously (and DEP0005 never - // fires on this throwing path, so the compiled silence matches). - // Every other new Buffer form keeps the fence below — the - // constructing forms would owe the deprecation warning. - if ( - expr.expression.text === "Buffer" && - expr.arguments?.nodes.length === 2 && - !expr.arguments.nodes.some(ts.isSpreadElement) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" - ) { - const first = lowerer.lowerExpr(expr.arguments.nodes[0]!); - if ( - first.kind === "unitLit" || - first.type.kind === "dyn" || - lowerer.dynConvertible(first.type) - ) { - const got: IrExpr = - first.type.kind === "dyn" ? first : { kind: "dynFrom", value: first, type: DYN, loc }; - // The encoding argument still evaluates in Node before the - // throw only via the ctor body's later reads — it does NOT - // observe it before throwing, so dropping it is exact for - // effect-free operands; effectful ones keep the fence. - const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); - if (enc.kind === "strLit" || isSafeToRepeat(enc)) { - return { - kind: "libCall", - fn: "buffer.newStringFail", - args: [got], - type: bytesOf("u8"), - loc, - }; - } - } - } const ctorHints: Record = { RegExp: "use a regex literal (/pattern/flags) — constructed regexes have no lowering", String: diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 5266d5c4e..7f0f3684a 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -14,7 +14,7 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { unsignedHex } from "../../format-integer.js"; -import type { Lowerer } from "./lowerer.js"; +import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; import { isJsSourceFile } from "../program.js"; import { BOOL, @@ -2416,6 +2416,12 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo } const decl = sym.declarations[0]; if (!decl) return null; + // A JavaScript function can hold own properties (`f.x = 1`, computed + // keys), which the runtime walk prints after its name. + if (isJsSourceFile(node.getSourceFile())) { + const type = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (type?.kind === "func" && lowerer.dynConvertible(type)) return null; + } if (ts.isFunctionDeclaration(decl) && decl.name) { return strLit(`[Function: ${decl.name.text}]`, loc); } @@ -2476,7 +2482,17 @@ export function lowerInspectValue( loc, ); } - const value = lowerer.lowerExpr(valueNode); + let value = lowerer.lowerExpr(valueNode); + // A JavaScript function renders through the runtime walk, which prints + // its own properties after its name. + if ( + value.type.kind === "func" && + isJsSourceFile(valueNode.getSourceFile()) && + lowerer.dynConvertible(value.type) + ) { + const name = jsFuncNameOf(valueNode); + value = { kind: "dynFrom", value, type: DYN, ...(name !== null ? { fnName: name } : {}), loc }; + } if (value.type.kind === "bytes") { // One runtime representation serves Buffer AND Uint8Array; their // renderings differ ( vs Uint8Array(1) [ 170 ]), so the diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e36ffd09f..c4b13c9ab 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -151,6 +151,7 @@ import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; import { guardClassAttributeWrites } from "./class-attributes.js"; import { applyHandleProperties } from "./handle-properties.js"; +import { installBufferValue } from "./containers/bytes.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; @@ -7698,6 +7699,7 @@ export class Lowerer { trackClassFieldCreation(this, functions); guardClassAttributeWrites(this, functions); applyHandleProperties(this, functions); + installBufferValue(functions); // Globals typed by a class that never REGISTERED (a JS class whose // collection fenced — Symbol-keyed fields, an unsupported base): the @@ -13870,6 +13872,9 @@ export class Lowerer { loc, }; } + // The buffer module's Buffer is the global constructor's value. + if (bi.module === "buffer" && bi.member === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native")) return fsRealpathValue(this, bi.member === "realpathSync.native", loc); const fn = builtinModuleFnOf(this, bi.module, bi.member); diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8b826304b..84d41e869 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -3445,6 +3445,9 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + // The buffer module's Buffer (`require("buffer").Buffer`, `buffer_1.Buffer`) + // is the global constructor. + if (bufferNamespaceMember(lowerer, expr)) return "Buffer"; if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; // Through a detection chain the checker types the receiver as the // union of every arm; the selected arm (the global object) owns the diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 68b649b07..c9c41fceb 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1378,6 +1378,11 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean { if (JS_ANY_OPERATOR_CODES.has(d.code)) { return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'"); } + // The deprecated Buffer(...) call without `new`, which Node's Buffer + // function accepts (containers/bytes.ts lowers it). + if (d.code === 2348) { + return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'"); + } return false; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 0239c1624..aead4b085 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -829,12 +829,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromDyn", "buffer.poolFromDynRef", "buffer.poolFromDynOffset", + "buffer.poolFromDynRange", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", "bytes.equalsChk", "bytes.compareChk", "buffer.newStringFail", + // Inherited and symbol-keyed Buffer.prototype members throw SC2020. + "buffer.prototypeMember", "fs.toUnixTimestamp", "fs.existsChk", "fs.mkdtempChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6a645785f..87ddda65d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2988,6 +2988,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** poolFromDyn that also copies an array crossing by reference. */ "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, + /** Buffer.from(value, byteOffset, length): an ArrayBuffer's range with + * Node's fromArrayBuffer checks; other values as poolFromDynOffset. */ + "buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like @@ -3022,6 +3025,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument * must be of type string. Received ..."). Borrowed dyn; may-throw. */ "buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 }, + "buffer.deprecation": { argTypes: [], result: VOID }, + "buffer.deprecationCount": { argTypes: [], result: VOID }, + "buffer.constructor": { argTypes: [], result: DYN }, + "buffer.installValue": { argTypes: [], result: VOID }, + "buffer.staticValue": { argTypes: [STRING], result: DYN }, + "buffer.prototypeMember": { argTypes: [DYN], result: DYN }, /** fs._toUnixTimestamp(time) over a dyn value: numeric strings and * finite numbers coerce (negatives answer now/1000, Node's shape); * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index df9e3c0a5..be76cee7c 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -432,6 +432,8 @@ const DYN_READ_ONLY = new Set([ "error.new", "error.newOptions", "fs.utimesSync", + // Buffer.prototype members read as values: a lookup by key. + "buffer.prototypeMember", "insp.dyn", "insp.dynS", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 819cfbee0..89ed37e21 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -98,6 +98,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "process.setDeprecationFlag", "process.setEmitWarning", "punycode.warn", + // DEP0005 for the deprecated Buffer constructor. + "buffer.deprecation", "timers.immediatePromise", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). @@ -358,6 +360,10 @@ const SYMBOL_LIB_FNS: ReadonlySet = new Set([ "dyn.usingDispose", "dyn.usingMethod", "util.isDeepStrictEqual", + // The Buffer function value's Symbol(kIsEncodingSymbol) member + "buffer.constructor", + "buffer.staticValue", + "buffer.prototypeMember", ]); /** Entry points that link the querystring unit. */ @@ -839,6 +845,7 @@ const RUNTIME_FEATURE_RULES = { ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, "util.promisifyDyn", + "buffer.prototypeMember", ], // Constant-key reads of Object.prototype member names answer the // inherited member through scr_dyn_invoke.c. @@ -867,10 +874,22 @@ const RUNTIME_FEATURE_RULES = { /** Buffer.prototype methods called on checked-dynamic values * (scr_bytes_dyn.c). */ dynBuffer: rule({ + // Buffer.prototype members read as values dispatch to the same entries. + fns: ["buffer.prototypeMember"], expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method), }), /** Builtin functions read as values (scr_builtin_values.c). */ - builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }), + builtinValues: rule({ + fns: [ + "buffer.byteLenValue", + "buffer.allocValue", + "fs.realpathValue", + "buffer.constructor", + "buffer.installValue", + "buffer.staticValue", + "buffer.prototypeMember", + ], + }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ /** scr_ecdh.c over the mbedTLS archive; DEP0031 reports through the diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json new file mode 100644 index 000000000..577923b2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7527-buffer-from-array-buffer.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json new file mode 100644 index 000000000..6ef4e3917 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7528-buffer-from-array-buffer.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json new file mode 100644 index 000000000..5b199cd4c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7529-buffer-constructor.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json new file mode 100644 index 000000000..1927980cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7530-buffer-constructor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json new file mode 100644 index 000000000..b760b018a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/7531-buffer-constructor-dependency/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'legacy-buf' module is not supported yet", + "loc": { + "file": "/tests/corpus/7531-buffer-constructor-dependency/main.cjs", + "start": 362, + "end": 392 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json new file mode 100644 index 000000000..e2f78ad2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/7532-buffer-constructor-check-limit/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'legacy-buf' module is not supported yet", + "loc": { + "file": "/tests/corpus/7532-buffer-constructor-check-limit/main.cjs", + "start": 279, + "end": 309 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json new file mode 100644 index 000000000..de7123599 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7533-buffer-value.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json new file mode 100644 index 000000000..03858cfce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7534-buffer-prototype-members.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json new file mode 100644 index 000000000..9831d7def --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7535-inspect-function-properties.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..8ea4877c6 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -252,6 +252,10 @@ void scr_process_set_emit_warning(ScrDyn *fn); * made from node_modules) — at once for a static ES import, which Node * links before any module evaluates, otherwise on the next tick. */ void scr_punycode_warn(bool immediate, bool report); +/* The deprecated Buffer() and new Buffer() called outside node_modules + * (scr_async_dyn.c): DEP0005 on the next tick, once, within Node's + * 10000-call check (scr_buffer_deprecation_count). */ +void scr_buffer_deprecation(void); /* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the * listener registration): units that cannot link the dyn-async unit route * their warnings through it, and print the default report otherwise. */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a6cd813de..909d9eb8a 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -948,6 +948,22 @@ void scr_punycode_warn(bool immediate, bool report) { scr_str_release(message); } +void scr_buffer_deprecation(void) { + /* Node's showFlaggedDeprecation: after 10000 calls the check stops. */ + if (scr_buffer_ctor_warned || ++scr_buffer_ctor_checks > 10000) return; + static const char text[] = + "Buffer() is deprecated due to security and usability issues. Please use the " + "Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead."; + ScrStr *message = scr_str_new(text, sizeof text - 1); + ScrStr *code = scr_str_new("DEP0005", 7); + ScrDyn *w = scr_warning_new(message, "DeprecationWarning", code); + scr_warning_emit_deferred(w); + scr_dyn_release(w); + scr_str_release(code); + scr_str_release(message); + scr_buffer_ctor_warned = true; +} + /* The console members' warnings. Node's emitWarning emits on * process.nextTick, so these queue in order and each rides one raw tick * on the shared tick queue; any left at exit are dropped with it. */ diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c index ea240359a..f8dd2b79b 100644 --- a/packages/runtime/src/scr_builtin_values.c +++ b/packages/runtime/src/scr_builtin_values.c @@ -259,3 +259,217 @@ ScrDyn *scr_fs_realpath_value(const ScrDyn *path, const ScrDyn *options, bool na scr_str_release(resolved); return out; } + +/* ── Buffer as a value ─────────────────────────────────────────────────── + * The Buffer function read as a value in JavaScript (bn.js assigns + * `require('buffer').Buffer` to a variable and passes it to + * `allocate(ArrayType, size)`): one function per thread, named Buffer + * with length 3, whose own enumerable properties are Node's, in Node's + * order: poolSize, the statics and Symbol(kIsEncodingSymbol). The statics + * apply Node's argument handling. Calling or constructing Buffer through + * the value, reading its prototype, and the copyBytesFrom and of statics + * throw SC2020. poolSize is a data property the pool reads before each + * decision (scr_buffer_pool_refresh_hook). */ +static SCR_TL ScrDyn *scr_bv_buffer; + +#define SCR_BV_ARG(i) ((i) < argc ? args[i] : scr_dyn_undefined()) + +static ScrDyn *scr_bv_unsupported(const char *what) { + char message[160]; + int n = snprintf(message, sizeof message, "%s through a stored Buffer value is not supported yet", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +/* A Buffer result as a value: +1, or NULL with the exception pending. */ +static ScrDyn *scr_bv_bytes(ScrBytes *bytes) { + if (!bytes) return NULL; + ScrBytes *buffer = scr_bytes_as_buffer(bytes); + scr_bytes_release(bytes); + ScrDyn *value = scr_dyn_new_bytes(buffer); + scr_bytes_release(buffer); + return value; +} + +static ScrDyn *scr_bv_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Calling or constructing Buffer"); +} + +static ScrDyn *scr_bv_from(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *value = SCR_BV_ARG(0), *second = SCR_BV_ARG(1), *third = SCR_BV_ARG(2); + if (value->kind != SCR_DYN_STR) return scr_bv_bytes(scr_buffer_from_dyn_range_pooled(value, second, third)); + /* fromString: an encoding that is not a non-empty string means utf8. */ + if (second->kind == SCR_DYN_STR && second->v.str->len > 0) + return scr_bv_bytes(scr_buffer_from_str_checked(value->v.str, second->v.str)); + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_buffer_from_str_checked(value->v.str, utf8); + scr_str_release(utf8); + return scr_bv_bytes(bytes); +} + +static ScrDyn *scr_bv_copy_bytes_from(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.copyBytesFrom"); +} + +static ScrDyn *scr_bv_of(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.of"); +} + +static ScrDyn *scr_bv_alloc(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return scr_bv_bytes(scr_buffer_alloc_value(SCR_BV_ARG(0), SCR_BV_ARG(1), SCR_BV_ARG(2))); +} + +static ScrDyn *scr_bv_alloc_unsafe(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *size = SCR_BV_ARG(0); + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_bv_bytes(scr_buffer_alloc_unsafe(size->v.num)); +} + +static ScrDyn *scr_bv_alloc_unsafe_slow(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *size = SCR_BV_ARG(0); + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_bv_bytes(scr_buffer_alloc_unsafe_slow(size->v.num)); +} + +static ScrDyn *scr_bv_is_buffer(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *value = SCR_BV_ARG(0); + return scr_dyn_new_bool(value->kind == SCR_DYN_BYTES && value->buffer); +} + +static ScrDyn *scr_bv_compare(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + double order = scr_buffer_compare_chk(SCR_BV_ARG(0), SCR_BV_ARG(1)); + return scr_exc_pending() ? NULL : scr_dyn_new_num(order); +} + +static ScrDyn *scr_bv_is_encoding(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *encoding = SCR_BV_ARG(0); + return scr_dyn_new_bool(encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && + scr_bytes_is_encoding(encoding->v.str)); +} + +/* Buffer.concat(list, length): Node validates the list, answers an empty + * Buffer for an empty list before it reads the length, and otherwise + * validates the length as an offset. */ +static ScrDyn *scr_bv_concat(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *list = SCR_BV_ARG(0), *length = SCR_BV_ARG(1); + bool empty = list->kind == SCR_DYN_ARR && list->v.arr.len == 0; + if (length->kind == SCR_DYN_UNDEF || empty || list->kind != SCR_DYN_ARR) + return scr_bv_bytes(scr_buffer_concat_dyn(list)); + if (length->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("length", "of type number", length); + return NULL; + } + return scr_bv_bytes(scr_buffer_concat_len_dyn(list, length->v.num)); +} + +static ScrDyn *scr_bv_byte_length(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + double length = scr_buffer_byte_length_value((ScrDyn *)SCR_BV_ARG(0), (ScrDyn *)SCR_BV_ARG(1)); + return scr_exc_pending() ? NULL : scr_dyn_new_num(length); +} + +static ScrDyn *scr_bv_prototype(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.prototype"); +} + +/* The pool's setting is the value's poolSize: a number as it is, another + * primitive through ToNumber, anything else NaN (no pooling). */ +static void scr_bv_pool_refresh(void) { + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + const ScrDyn *size = scr_dyn_obj_get(table, "poolSize", 8); + double setting = NAN; + if (size && size->kind == SCR_DYN_NUM) { + setting = size->v.num; + } else if (size && (size->kind == SCR_DYN_STR || size->kind == SCR_DYN_BOOL || size->kind == SCR_DYN_NULL || + size->kind == SCR_DYN_UNDEF)) { + if (!scr_dyn_number_coerce_js(size, &setting)) setting = NAN; + } + scr_dyn_release(table); + scr_buffer_pool_apply(setting); +} + +/* A static `Buffer.poolSize = n` write updates the value's property. */ +static void scr_bv_pool_publish(double size) { + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(size)); + scr_dyn_release(table); +} + +static void scr_bv_cleanup(void) { + scr_buffer_pool_refresh_hook = NULL; + scr_buffer_pool_publish_hook = NULL; + scr_dyn_release(scr_bv_buffer); + scr_bv_buffer = NULL; +} + +ScrDyn *scr_buffer_constructor_value(void) { + if (scr_bv_buffer) return scr_dyn_retain(scr_bv_buffer); + static const struct { const char *name; uint32_t length; ScrDynThunk thunk; } statics[] = { + {"from", 3, scr_bv_from}, + {"copyBytesFrom", 3, scr_bv_copy_bytes_from}, + {"of", 0, scr_bv_of}, + {"alloc", 3, scr_bv_alloc}, + {"allocUnsafe", 1, scr_bv_alloc_unsafe}, + {"allocUnsafeSlow", 1, scr_bv_alloc_unsafe_slow}, + {"isBuffer", 1, scr_bv_is_buffer}, + {"compare", 2, scr_bv_compare}, + {"isEncoding", 1, scr_bv_is_encoding}, + {"concat", 2, scr_bv_concat}, + {"byteLength", 2, scr_bv_byte_length}, + }; + double pool_size = scr_buffer_pool_size(); + scr_bv_buffer = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_call, 3, "native:Buffer", "Buffer"); + scr_atexit(scr_bv_cleanup); + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + ScrDyn *getter = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_prototype, 0, "native:function", "get prototype"); + scr_dyn_obj_set_accessor(table, "prototype", 9, getter, NULL); + ScrDynEntry *entry = &table->v.obj.entries[table->v.obj.len - 1]; + entry->enumerable = entry->configurable = false; + scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(pool_size)); + ScrDyn *is_encoding = NULL; + for (size_t i = 0; i < sizeof statics / sizeof statics[0]; i++) { + ScrDyn *fn = scr_dyn_new_func(scr_closure_new(NULL, 0), statics[i].thunk, statics[i].length, "native:function", + statics[i].name); + if (statics[i].thunk == scr_bv_is_encoding) is_encoding = scr_dyn_retain(fn); + scr_dyn_obj_set(table, statics[i].name, strlen(statics[i].name), fn); + } + scr_dyn_release(table); + ScrStr *description = scr_str_new("kIsEncodingSymbol", 17); + ScrSym *symbol = scr_sym_new(description); + scr_str_release(description); + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_sym_release(symbol); + scr_dyn_symbol_key_set(scr_bv_buffer, key, is_encoding); + scr_dyn_release(key); + scr_dyn_release(is_encoding); + scr_buffer_pool_refresh_hook = scr_bv_pool_refresh; + scr_buffer_pool_publish_hook = scr_bv_pool_publish; + return scr_dyn_retain(scr_bv_buffer); +} + +void scr_buffer_install_value(void) { scr_bytes_buffer_constructor_fn = scr_buffer_constructor_value; } + +ScrDyn *scr_buffer_static_value(const ScrStr *name) { + ScrDyn *buffer = scr_buffer_constructor_value(); + ScrDyn *member = scr_dyn_fn_get(buffer, name->data, name->len); + scr_dyn_release(buffer); + return member; +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7bcfd0cfb..907c177dc 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -2787,16 +2787,30 @@ static SCR_TL bool scr_pool_registered; /* scr_buffer_pool_reset runs at context /* Buffer.poolSize >>> 1, kept with the setting. */ static SCR_TL size_t scr_pool_half = 32768; -double scr_buffer_pool_size(void) { return scr_pool_setting; } +SCR_TL void (*scr_buffer_pool_refresh_hook)(void) = NULL; +SCR_TL void (*scr_buffer_pool_publish_hook)(double size) = NULL; -void scr_buffer_set_pool_size(double size) { +double scr_buffer_pool_size(void) { + if (scr_buffer_pool_refresh_hook) scr_buffer_pool_refresh_hook(); + return scr_pool_setting; +} + +void scr_buffer_pool_apply(double size) { scr_pool_setting = size; double t = isfinite(size) ? fmod(trunc(size), 4294967296.0) : 0; if (t < 0) t += 4294967296.0; scr_pool_half = (size_t)((uint32_t)t >> 1); } -static size_t scr_pool_threshold(void) { return scr_pool_half; } +void scr_buffer_set_pool_size(double size) { + scr_buffer_pool_apply(size); + if (scr_buffer_pool_publish_hook) scr_buffer_pool_publish_hook(size); +} + +static size_t scr_pool_threshold(void) { + if (scr_buffer_pool_refresh_hook) scr_buffer_pool_refresh_hook(); + return scr_pool_half; +} /* Bytes left in the slab. Aligning can move the offset past the end of a * slab whose size is not a multiple of 8; nothing is left then. */ diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9b382dda5..cca8364b1 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -388,6 +388,13 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc); * slab. A slab lives while any of its views does. All return +1 Buffers. */ double scr_buffer_pool_size(void); void scr_buffer_set_pool_size(double size); +/* The setting without publishing it (scr_buffer_pool_refresh_hook). */ +void scr_buffer_pool_apply(double size); +/* Set while a Buffer function value exists (scr_builtin_values.c): its + * poolSize property is the setting, which the refresh hook reads before + * each pool decision and the publish hook receives from static writes. */ +extern SCR_TL void (*scr_buffer_pool_refresh_hook)(void); +extern SCR_TL void (*scr_buffer_pool_publish_hook)(double size); ScrBytes *scr_buffer_alloc(double size); /* zeroed, never pooled */ ScrBytes *scr_buffer_alloc_unsafe(double size); /* pooled */ ScrBytes *scr_buffer_alloc_unsafe_slow(double size); /* never pooled */ @@ -398,6 +405,15 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second); +/* Buffer.from(value, byteOffset, length): an ArrayBuffer's view with + * Node's fromArrayBuffer checks; other values as the two-argument form. */ +ScrBytes *scr_buffer_from_dyn_range_pooled(const ScrDyn *value, const ScrDyn *second, + const ScrDyn *third); +/* The deprecated Buffer constructor's DEP0005 state (scr_bytes_io.c): a + * call from node_modules counts toward Node's 10000-call check. */ +extern SCR_TL bool scr_buffer_ctor_warned; +extern SCR_TL uint64_t scr_buffer_ctor_checks; +void scr_buffer_deprecation_count(void); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list @@ -406,6 +422,17 @@ ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); ScrBytes *scr_buffer_alloc_value(const struct ScrDyn *size, const struct ScrDyn *fill, const struct ScrDyn *encoding); struct ScrDyn *scr_fs_realpath_value(const struct ScrDyn *path, const struct ScrDyn *options, bool native); +/* The Buffer function as a value (scr_builtin_values.c), one per thread. +1. */ +struct ScrDyn *scr_buffer_constructor_value(void); +/* Installs that function as a Buffer's `.constructor` (the program's + * %main calls it when it reads the value): scr_bytes_buffer_constructor_fn. */ +void scr_buffer_install_value(void); +extern struct ScrDyn *(*scr_bytes_buffer_constructor_fn)(void); +/* A static of that value by name (`Buffer.alloc` read as a value). +1. */ +struct ScrDyn *scr_buffer_static_value(const ScrStr *name); +/* `Buffer.prototype[key]` (scr_bytes_dyn.c). +1, or NULL with the + * exception pending. */ +struct ScrDyn *scr_buffer_prototype_member(const struct ScrDyn *key); ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index c492d43b1..a65cdf239 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -592,3 +592,206 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, } return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } + +/* A fixed-width read or write name (readUInt8, writeDoubleLE, ...). */ +static bool scr_bytes_dyn_num_method_name(const char *method) { + bool read = !strncmp(method, "read", 4); + if (!read && strncmp(method, "write", 5)) return false; + const char *rest = method + (read ? 4 : 5); + for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) + if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return true; + return false; +} + +/* ── Buffer.prototype members read as values ──────────────────────────── + * `Buffer.prototype[name]` (smart-buffer's typeof probe for the BigInt + * methods): an own member answers one function per name, with the name and + * length Node's function has; the parent and offset getters, read on the + * prototype itself, answer undefined; constructor is the Buffer value. + * Members inherited from Uint8Array.prototype and Object.prototype, symbol + * keys and object keys throw SC2020, and other keys answer undefined. The + * functions run the method on a Buffer receiver (`.call(buf, ...)`); other + * receivers throw SC2020. */ +static const struct { const char *key; const char *name; uint32_t length; } scr_buffer_proto_own[] = { + {"readBigUInt64LE", "readBigUInt64LE", 0}, + {"readBigUInt64BE", "readBigUInt64BE", 0}, + {"readBigUint64LE", "readBigUInt64LE", 0}, + {"readBigUint64BE", "readBigUInt64BE", 0}, + {"readBigInt64LE", "readBigInt64LE", 0}, + {"readBigInt64BE", "readBigInt64BE", 0}, + {"writeBigUInt64LE", "writeBigUInt64LE", 1}, + {"writeBigUInt64BE", "writeBigUInt64BE", 1}, + {"writeBigUint64LE", "writeBigUInt64LE", 1}, + {"writeBigUint64BE", "writeBigUInt64BE", 1}, + {"writeBigInt64LE", "writeBigInt64LE", 1}, + {"writeBigInt64BE", "writeBigInt64BE", 1}, + {"readUIntLE", "readUIntLE", 2}, + {"readUInt32LE", "", 1}, + {"readUInt16LE", "", 1}, + {"readUInt8", "", 1}, + {"readUIntBE", "readUIntBE", 2}, + {"readUInt32BE", "", 1}, + {"readUInt16BE", "", 1}, + {"readUintLE", "readUIntLE", 2}, + {"readUint32LE", "", 1}, + {"readUint16LE", "", 1}, + {"readUint8", "", 1}, + {"readUintBE", "readUIntBE", 2}, + {"readUint32BE", "", 1}, + {"readUint16BE", "", 1}, + {"readIntLE", "readIntLE", 2}, + {"readInt32LE", "", 1}, + {"readInt16LE", "", 1}, + {"readInt8", "", 1}, + {"readIntBE", "readIntBE", 2}, + {"readInt32BE", "", 1}, + {"readInt16BE", "", 1}, + {"writeUIntLE", "writeUIntLE", 3}, + {"writeUInt32LE", "writeUInt32LE", 1}, + {"writeUInt16LE", "writeUInt16LE", 1}, + {"writeUInt8", "writeUInt8", 1}, + {"writeUIntBE", "writeUIntBE", 3}, + {"writeUInt32BE", "writeUInt32BE", 1}, + {"writeUInt16BE", "writeUInt16BE", 1}, + {"writeUintLE", "writeUIntLE", 3}, + {"writeUint32LE", "writeUInt32LE", 1}, + {"writeUint16LE", "writeUInt16LE", 1}, + {"writeUint8", "writeUInt8", 1}, + {"writeUintBE", "writeUIntBE", 3}, + {"writeUint32BE", "writeUInt32BE", 1}, + {"writeUint16BE", "writeUInt16BE", 1}, + {"writeIntLE", "writeIntLE", 3}, + {"writeInt32LE", "writeInt32LE", 1}, + {"writeInt16LE", "writeInt16LE", 1}, + {"writeInt8", "writeInt8", 1}, + {"writeIntBE", "writeIntBE", 3}, + {"writeInt32BE", "writeInt32BE", 1}, + {"writeInt16BE", "writeInt16BE", 1}, + {"readFloatLE", "readFloatForwards", 0}, + {"readFloatBE", "readFloatBackwards", 0}, + {"readDoubleLE", "readDoubleForwards", 0}, + {"readDoubleBE", "readDoubleBackwards", 0}, + {"writeFloatLE", "writeFloatForwards", 1}, + {"writeFloatBE", "writeFloatBackwards", 1}, + {"writeDoubleLE", "writeDoubleForwards", 1}, + {"writeDoubleBE", "writeDoubleBackwards", 1}, + {"asciiSlice", "", 2}, + {"base64Slice", "", 2}, + {"base64urlSlice", "", 2}, + {"latin1Slice", "", 2}, + {"hexSlice", "", 2}, + {"ucs2Slice", "", 2}, + {"utf8Slice", "", 2}, + {"asciiWrite", "", 3}, + {"base64Write", "", 3}, + {"base64urlWrite", "", 3}, + {"latin1Write", "", 3}, + {"hexWrite", "", 3}, + {"ucs2Write", "", 3}, + {"utf8Write", "", 3}, + {"parent", NULL, 0}, + {"offset", NULL, 0}, + {"copy", "copy", 4}, + {"toString", "toString", 3}, + {"equals", "equals", 1}, + {"inspect", "inspect", 2}, + {"compare", "compare", 5}, + {"indexOf", "indexOf", 4}, + {"lastIndexOf", "lastIndexOf", 4}, + {"includes", "includes", 4}, + {"fill", "fill", 4}, + {"write", "write", 4}, + {"toJSON", "toJSON", 0}, + {"subarray", "subarray", 2}, + {"slice", "slice", 2}, + {"swap16", "swap16", 0}, + {"swap32", "swap32", 0}, + {"swap64", "swap64", 0}, + {"toLocaleString", "toString", 3}, +}; + +static const char *const scr_buffer_proto_inherited[] = { + "BYTES_PER_ELEMENT", "buffer", "byteLength", "byteOffset", "length", "entries", "keys", "values", + "at", "copyWithin", "every", "filter", "find", "findIndex", "findLast", "findLastIndex", + "forEach", "join", "map", "reverse", "reduce", "reduceRight", "set", "some", "sort", "toReversed", + "toSorted", "with", "__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", + "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable", "valueOf", "__proto__", +}; + +static SCR_TL ScrDyn *scr_buffer_proto_fns; + +static void scr_buffer_proto_cleanup(void) { + scr_dyn_release(scr_buffer_proto_fns); + scr_buffer_proto_fns = NULL; +} + +static ScrDyn *scr_buffer_proto_unsupported(const char *format, const char *name) { + char message[200]; + int n = snprintf(message, sizeof message, format, name); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +static ScrDyn *scr_buffer_proto_run(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc) { + if (!scr_bytes_dyn_buffer(recv)) + return scr_buffer_proto_unsupported("Buffer.prototype.%s on a receiver that is not a Buffer is not supported yet", + method); + const char *what = "Buffer.prototype method"; + /* The method-id cache slot of a call site, here a fresh one per call. */ + int32_t slot = -1; + int32_t *mid = &slot; + if (scr_bytes_dyn_num_method_name(method)) return scr_dyn_invoke_buffer_num(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "write")) return scr_dyn_invoke_buffer_write(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "fill")) return scr_dyn_invoke_buffer_fill(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "toString") || !strcmp(method, "toLocaleString")) + return scr_dyn_invoke_buffer_to_string(recv, NULL, "toString", mid, args, argc, what); + if (!strcmp(method, "indexOf") || !strcmp(method, "lastIndexOf") || !strcmp(method, "includes")) + return scr_dyn_invoke_buffer_search(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "subarray") || !strcmp(method, "slice")) + return scr_dyn_invoke(recv, method, args, argc, what); + if (!strcmp(method, "inspect")) + return scr_buffer_proto_unsupported("Buffer.prototype.%s through a stored value is not supported yet", method); + return scr_dyn_invoke_buffer(recv, NULL, method, mid, args, argc, what); +} + +static ScrDyn *scr_buffer_proto_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *recv = scr_dyn_this_get(); + ScrDyn *result = scr_buffer_proto_run(recv, closure->fn, args, argc); + scr_dyn_release(recv); + return result; +} + +ScrDyn *scr_buffer_prototype_member(const ScrDyn *key) { + if (key->kind == SCR_DYN_SYMBOL) + return scr_buffer_proto_unsupported("Buffer.prototype members keyed by %s are not supported yet", "symbols"); + if (key->kind != SCR_DYN_STR) { + if (key->kind == SCR_DYN_NUM || key->kind == SCR_DYN_BOOL || key->kind == SCR_DYN_UNDEF || + key->kind == SCR_DYN_NULL || key->kind == SCR_DYN_BIGINT) + return scr_dyn_retain(scr_dyn_undefined()); + return scr_buffer_proto_unsupported("Buffer.prototype members keyed by %s are not supported yet", "objects"); + } + const ScrStr *name = key->v.str; + if (name->len == 11 && memcmp(name->data, "constructor", 11) == 0) return scr_buffer_constructor_value(); + for (size_t i = 0; i < sizeof scr_buffer_proto_inherited / sizeof scr_buffer_proto_inherited[0]; i++) { + const char *inherited = scr_buffer_proto_inherited[i]; + if (strlen(inherited) == name->len && memcmp(inherited, name->data, name->len) == 0) + return scr_buffer_proto_unsupported("Buffer.prototype.%s is not supported yet", inherited); + } + for (size_t i = 0; i < sizeof scr_buffer_proto_own / sizeof scr_buffer_proto_own[0]; i++) { + const char *own = scr_buffer_proto_own[i].key; + if (strlen(own) != name->len || memcmp(own, name->data, name->len) != 0) continue; + if (!scr_buffer_proto_own[i].name) return scr_dyn_retain(scr_dyn_undefined()); + if (!scr_buffer_proto_fns) { + scr_buffer_proto_fns = scr_dyn_new_obj_null_proto(); + scr_atexit(scr_buffer_proto_cleanup); + } + ScrDyn *fn = scr_dyn_obj_get(scr_buffer_proto_fns, own, name->len); + if (!fn) { + fn = scr_dyn_new_func(scr_closure_new((void *)own, 0), scr_buffer_proto_call, scr_buffer_proto_own[i].length, + "native:function", scr_buffer_proto_own[i].name); + scr_dyn_obj_set(scr_buffer_proto_fns, own, name->len, fn); + } + return scr_dyn_retain(fn); + } + return scr_dyn_retain(scr_dyn_undefined()); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 137208938..c93c67cdc 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,13 +1299,16 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } -/* Buffer.from(arrayBuffer, byteOffset) over a checked ArrayBuffer or - * SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer from the offset to - * the end of the buffer that shares its memory. The offset converts with - * ToNumber, where a missing offset (NULL) or undefined and NaN are 0; an - * offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS, and one at or below - * -1 V8's start-offset RangeError. NULL with the exception pending. */ -static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value) { +/* Buffer.from(arrayBuffer, byteOffset, length) over a checked ArrayBuffer + * or SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer that shares its + * memory. The offset converts with ToNumber, where a missing offset (NULL) + * or undefined and NaN are 0; an offset past the end throws + * ERR_BUFFER_OUT_OF_BOUNDS, and one at or below -1 V8's start-offset + * RangeError. A missing length runs to the end; a given one converts with + * ToNumber, past the end throws ERR_BUFFER_OUT_OF_BOUNDS for "length", and + * zero, negative and NaN lengths are 0. NULL with the exception pending. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value, + const ScrDyn *length_value) { double offset = 0; if (offset_value && offset_value->kind != SCR_DYN_UNDEF) { if (!scr_dyn_number_coerce_js(offset_value, &offset)) return NULL; @@ -1313,11 +1316,26 @@ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *of } ScrBytes *bytes = storage->v.handle.ptr; ScrBytes *owner = bytes->backing ? bytes->backing : bytes; - if ((double)scr_bytes_byte_len(owner) - offset < 0) { + double max_length = (double)scr_bytes_byte_len(owner) - offset; + if (max_length < 0) { static const char msg[] = "\"offset\" is outside of buffer bounds"; scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); return NULL; } + bool has_length = length_value && length_value->kind != SCR_DYN_UNDEF; + double length = max_length; + if (has_length) { + if (!scr_dyn_number_coerce_js(length_value, &length)) return NULL; + if (length > 0) { + if (length > max_length) { + static const char msg[] = "\"length\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + } else { + length = 0; + } + } double start = trunc(offset); if (start < 0) { /* The offset as JavaScript prints it (-1.5, -1e+21, -Infinity). */ @@ -1331,7 +1349,7 @@ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *of scr_throw_error(SCR_ERR_RANGE, message); return NULL; } - ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, false, 0); + ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, has_length, trunc(length)); if (out) out->is_buffer = true; return out; } @@ -1340,7 +1358,7 @@ ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); /* The second argument here is an encoding name, which as a byte offset * converts to NaN: offset 0. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL, NULL); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1386,7 +1404,7 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding return scr_buffer_from_bytes(value->v.bytes); /* A view of an ArrayBuffer shares its memory, so it never comes from the * pool. The second argument here is an encoding name: offset 0. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL, NULL); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; @@ -1422,12 +1440,32 @@ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *s scr_str_release(utf8); return out; } - return scr_buffer_from_storage(value, second); + return scr_buffer_from_storage(value, second, NULL); +} + +/* Buffer.from(value, second, third): an ArrayBuffer or SharedArrayBuffer + * takes them as its byte offset and length; other values convert as + * scr_buffer_from_dyn_offset_pooled does, without the third argument. */ +ScrBytes *scr_buffer_from_dyn_range_pooled(const ScrDyn *value, const ScrDyn *second, + const ScrDyn *third) { + if (!scr_buffer_storage_is(value)) return scr_buffer_from_dyn_offset_pooled(value, second); + return scr_buffer_from_storage(value, second, third); +} + +/* The deprecated Buffer constructor's DEP0005 state, Node's + * bufferWarningAlreadyEmitted and nodeModulesCheckCounter: a call from + * node_modules only counts toward the 10000-call check, and + * scr_buffer_deprecation (scr_async_dyn.c) reports for the others. */ +SCR_TL bool scr_buffer_ctor_warned = false; +SCR_TL uint64_t scr_buffer_ctor_checks = 0; + +void scr_buffer_deprecation_count(void) { + if (!scr_buffer_ctor_warned) scr_buffer_ctor_checks++; } /* new Buffer(number, encoding) — the deprecated ctor's string arm with a - * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE (the - * throwing path never fires DEP0005, so compiled silence matches). */ + * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE, thrown + * after scr_buffer_deprecation has reported DEP0005. */ ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got) { scr_dyn_arg_type_fail("string", "of type string", got); return NULL; diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..e93a9a0f0 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1328,6 +1328,10 @@ ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, si * for anonymous) and "length" (the boxed arity). Returns +1, or NULL when * the key answers nothing (the caller's undefined). Never throws. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len); +/* A function value's own property table, created on first use. +1. */ +ScrDyn *scr_dyn_function_properties(const ScrDyn *function); +/* The table a function value already has. +1, or NULL. */ +ScrDyn *scr_dyn_function_properties_existing(const ScrDyn *function); /* Object.defineProperties over dyn values (targets: OBJ and FUNC): each * descriptor's `value` becomes an own data property. OBJ targets preserve * writable/enumerable/configurable; FUNC targets still use plain properties. diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 1dce8d3c2..79b2564ff 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1600,7 +1600,45 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } else { ib_cstr(&out, "[Function (anonymous)]"); } - return ib_take(&out); + /* A function's own enumerable properties follow its base, as for an + * object (`[Function: f] { x: 1 }`), and past the depth limit it is + * `[Function]` (formatRaw). Classes keep the base. */ + ScrDyn *table = d->v.fn.class_obj ? NULL : scr_dyn_function_properties_existing(d); + bool shown = false; + for (size_t i = 0; table && i < table->v.obj.len && !shown; i++) shown = table->v.obj.entries[i].enumerable; + for (size_t i = 0; table && !shown && table->symbol_keys && i < table->symbol_keys->v.arr.len; i++) + shown = scr_insp_symbol_entry(table, i) != NULL; + if (!shown) { + scr_dyn_release(table); + return ib_take(&out); + } + /* Every value of one function shares its table: the table is the + * function's identity for the circular checks. */ + double circular = scr_insp_circ_check(table); + if (circular != 0) { + scr_dyn_release(table); + free(out.data); + return scr_insp_circular(circular); + } + if (recurse > depth) { + scr_dyn_release(table); + free(out.data); + return scr_str_new("[Function]", 10); + } + ScrStr *base = ib_take(&out); + char *cls = malloc(base->len + 1); + if (!cls) scr_trap("scriptc: out of memory\n"); + memcpy(cls, base->data, base->len); + cls[base->len] = '\0'; + scr_str_release(base); + size_t nseen = g_nseen; + g_pending_seen = table; + ScrStr *rendered = scr_insp_obj(table, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(cls); + scr_dyn_release(table); + return rendered; } case SCR_DYN_HANDLE: { if (d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..c0154ab57 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2366,6 +2366,10 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { return owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL; } +ScrDyn *scr_dyn_function_properties_existing(const ScrDyn *function) { + return scr_dyn_fn_existing_table(function); +} + ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } @@ -4200,6 +4204,9 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc } if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); + /* The Buffer function value: Buffer instances, not other Uint8Arrays. */ + if (callee->v.fn.sig && strcmp(callee->v.fn.sig, "native:Buffer") == 0) + return value->kind == SCR_DYN_BYTES && value->buffer; for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); @@ -4262,6 +4269,7 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { return scr_bytes_instanceof_at(value, callee, NULL); } SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = NULL; +ScrDyn *(*scr_bytes_buffer_constructor_fn)(void) = NULL; ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) @@ -4282,6 +4290,14 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS ScrDyn *constructed = NULL; if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed)) return constructed; + /* The Buffer function value (scr_builtin_values.c): Node's construction + * reports DEP0005 by the caller's location, which a stored value does + * not know. */ + if (callee->kind == SCR_DYN_FUNC && callee->v.fn.sig && strcmp(callee->v.fn.sig, "native:Buffer") == 0) { + static const char message[] = "Calling or constructing Buffer through a stored Buffer value is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } ScrJsonBuf message; scr_jb_init(&message); scr_jb_write(&message, what->data, what->len); @@ -4311,7 +4327,9 @@ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { return scr_dyn_new_num((double)scr_bytes_elem_size(bytes->elem)); if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { if (!value->buffer) return scr_bytes_constructor_for(bytes->elem); - /* Buffer retains its separate builtin identity. */ + /* A program that reads the Buffer function's value answers it; + * otherwise Buffer keeps its separate builtin identity token. */ + if (scr_bytes_buffer_constructor_fn) return scr_bytes_buffer_constructor_fn(); char token[64]; int length = snprintf(token, sizeof token, "[builtin %s]", value->buffer ? "Buffer" : scr_bytes_elem_name(bytes->elem)); ScrStr *name = scr_str_new(token, (size_t)length); @@ -5219,6 +5237,8 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { return (ScrDyn *)scr_box_get_ref(owner->props); } +ScrDyn *scr_dyn_function_properties(const ScrDyn *function) { return scr_dyn_fn_properties(function); } + ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { if (constructor->kind != SCR_DYN_FUNC || !constructor->v.fn.class_obj || (base->kind != SCR_DYN_FUNC && base->kind != SCR_DYN_UNDEF)) { static const char message[] = "Class inheritance requires native constructors"; diff --git a/tests/corpus/2570-buffer-arg-validation.cjs b/tests/corpus/2570-buffer-arg-validation.cjs index d49593a30..c0980e495 100644 --- a/tests/corpus/2570-buffer-arg-validation.cjs +++ b/tests/corpus/2570-buffer-arg-validation.cjs @@ -1,4 +1,3 @@ -// @no-deprecation // Buffer's invalid-argument ladders, Node-exact: Buffer.compare / // buf.compare / buf.equals over non-buffer values and bad offsets throw // ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE with Node's argument names and @@ -7,6 +6,8 @@ // real answers. Plus the deprecated `new Buffer(number, enc)` string-arm // rejection and the ERR_* codes on the write/swap/fill range ladders. 'use strict'; +// The deprecated constructor's DEP0005 report carries a process id. +process.noDeprecation = true; const show = (fn) => { try { console.log('ret', fn()); diff --git a/tests/corpus/7527-buffer-from-array-buffer.ts b/tests/corpus/7527-buffer-from-array-buffer.ts new file mode 100644 index 000000000..fb650e9f2 --- /dev/null +++ b/tests/corpus/7527-buffer-from-array-buffer.ts @@ -0,0 +1,33 @@ +// Buffer.from(arrayBuffer, byteOffset, length): Node's fromArrayBuffer over +// typed and untyped ArrayBuffers and SharedArrayBuffers: the offset and +// length conversions, the shared memory, zero and negative lengths, and +// ERR_BUFFER_OUT_OF_BOUNDS for an offset or length past the end. +const ab = new ArrayBuffer(6); +new Uint8Array(ab).set([1, 2, 3, 4, 5, 6]); +const offsets: unknown[] = [undefined, NaN, "1", 1.5, -1, 0, 6, 7, null, true]; +const lengths: unknown[] = [undefined, 0, -1, 2.5, "2", 6, 9, NaN, null]; +for (const o of offsets) { + for (const l of lengths) { + try { + const b = Buffer.from(ab, o as number, l as number); + console.log(String(o), String(l), b, b.byteOffset, b.length); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(String(o), String(l), err.name, err.code, err.message); + } + } +} +const typed = Buffer.from(ab, 2, 2); +typed[0] = 99; +console.log(new Uint8Array(ab)[2], Buffer.from(ab, 4).toString("hex"), Buffer.isBuffer(Buffer.from(ab, 1, 1))); +const shared = new SharedArrayBuffer(4); +console.log(Buffer.from(shared, 1, 2).length); +const loose: any = new ArrayBuffer(4); +const pairs: [unknown, unknown][] = [[1, 2], [4, undefined], [5, 1], [0, 5], ["2", "1"]]; +for (const [o, l] of pairs) { + try { + console.log(Buffer.from(loose, o as number, l as number).toString("hex")); + } catch (e) { + console.log((e as NodeJS.ErrnoException).code, (e as Error).message); + } +} diff --git a/tests/corpus/7528-buffer-from-array-buffer.cjs b/tests/corpus/7528-buffer-from-array-buffer.cjs new file mode 100644 index 000000000..e77b5d55b --- /dev/null +++ b/tests/corpus/7528-buffer-from-array-buffer.cjs @@ -0,0 +1,31 @@ +// Buffer.from(arrayBuffer, byteOffset, length) in JavaScript, with offsets +// and lengths of every kind and an ArrayBuffer held in an untyped slot. +const ab = new ArrayBuffer(6); +new Uint8Array(ab).set([1, 2, 3, 4, 5, 6]); +const offsets = [undefined, NaN, "1", 1.5, -1, 0, 6, 7, null, true]; +const lengths = [undefined, 0, -1, 2.5, "2", 6, 9, NaN, null]; +for (const o of offsets) { + for (const l of lengths) { + try { + const b = Buffer.from(ab, o, l); + console.log(String(o), String(l), b, b.byteOffset, b.length); + } catch (e) { + const err = e; + console.log(String(o), String(l), err.name, err.code, err.message); + } + } +} +const typed = Buffer.from(ab, 2, 2); +typed[0] = 99; +console.log(new Uint8Array(ab)[2], Buffer.from(ab, 4).toString("hex"), Buffer.isBuffer(Buffer.from(ab, 1, 1))); +const shared = new SharedArrayBuffer(4); +console.log(Buffer.from(shared, 1, 2).length); +const slots = { buffer: new ArrayBuffer(3) }; +new Uint8Array(slots.buffer).set([9, 8, 7]); +for (const args of [[1], [1, 1], [0, 0], [3, 0], [2, 5], [-0.5, 2]]) { + try { + console.log(args.join(","), Buffer.from(slots.buffer, args[0], args[1])); + } catch (e) { + console.log(args.join(","), e.code, e.message); + } +} diff --git a/tests/corpus/7529-buffer-constructor.cjs b/tests/corpus/7529-buffer-constructor.cjs new file mode 100644 index 000000000..3181cebec --- /dev/null +++ b/tests/corpus/7529-buffer-constructor.cjs @@ -0,0 +1,80 @@ +// The deprecated Buffer constructor, called and constructed, as Node's +// Buffer function runs it: the arguments evaluate in order, the first call +// reports DEP0005 on the next tick (once), a number allocates zero-filled +// bytes, and other values convert as Buffer.from does. +// The program re-runs itself and prints the child's output with process +// ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const { Buffer: Imported } = require("buffer"); + +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code, w.message.length)); + const B = Buffer; + + const show = (label, fn) => { + try { + const b = fn(); + console.log(label, b, b.length, Buffer.isBuffer(b)); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } + }; + + // The throwing form still reports the warning, after the catch. + show("number+encoding", () => new Buffer(3, "utf8")); + show("size", () => new Buffer(4)); + show("fraction", () => new Buffer(2.7)); + show("zero", () => Buffer(0)); + show("negative", () => new Buffer(-1)); + show("nan", () => new Buffer(NaN)); + show("string", () => new Buffer("héllo")); + show("hex", () => new Buffer("6869", "hex")); + show("base64", () => Buffer("aGk=", "base64")); + show("empty encoding", () => new Buffer("x", "")); + show("bad encoding", () => new Buffer("x", "nope")); + show("array", () => new Buffer([1, 2, 300, -1])); + show("array-like", () => new Buffer({ length: 2, 0: 7, 1: 8 })); + show("buffer", () => new B(Buffer.from("copy"))); + show("typed", () => Buffer(new Uint16Array([1, 258]))); + const storage = new Uint8Array([10, 20, 30, 40, 50]).buffer; + show("arraybuffer", () => new Buffer(storage)); + show("offset", () => new Buffer(storage, 1)); + show("range", () => Buffer(storage, 1, 3)); + show("range past end", () => new Buffer(storage, 6)); + show("string third", () => new Buffer("abc", "latin1", 1)); + show("undefined", () => new Buffer(undefined)); + show("null", () => Buffer(null)); + show("none", () => new Buffer()); + show("imported", () => new Imported("imp")); + + console.log(Buffer(2), Buffer("ab").length, Buffer("hi").toString("hex"), new Buffer([5])); + console.log(Buffer.isBuffer(Buffer(1)), Buffer(1) instanceof Uint8Array, new Buffer(1) instanceof Uint8Array); + + // The view shares the ArrayBuffer's memory. + const view = new Buffer(storage, 2, 2); + new Uint8Array(storage)[2] = 99; + console.log(view); + + const order = []; + const arg = (v) => (order.push(v), v); + new Buffer(arg("a"), arg("utf8")); + Buffer(arg(2)); + console.log(order); + + console.log("sync end"); + process.nextTick(() => console.log("tick")); + setTimeout(() => { + new Buffer(1); + console.log("timer"); + }, 0); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7530-buffer-constructor.ts b/tests/corpus/7530-buffer-constructor.ts new file mode 100644 index 000000000..14eac61d5 --- /dev/null +++ b/tests/corpus/7530-buffer-constructor.ts @@ -0,0 +1,42 @@ +// The deprecated `new Buffer(...)` in TypeScript, over the forms +// @types/node declares: strings with an encoding, sizes, arrays and +// ArrayBuffers. The first call reports DEP0005 on the next tick. +// The program re-runs itself and prints the child's output with process +// ids normalized. +import { spawnSync } from "node:child_process"; + +function child(): void { + process.on("warning", (w) => console.log("listener", w.name, (w as { code?: string }).code)); + + function sized(n: number): Buffer { + return new Buffer(n); + } + + const encodings: BufferEncoding[] = ["utf8", "hex", "base64", "latin1"]; + for (const encoding of encodings) { + console.log(encoding, new Buffer("6869", encoding)); + } + console.log(sized(3), sized(0).length); + console.log(new Buffer([104, 105]).toString(), new Buffer(new Uint8Array([1, 2]))); + const storage = new ArrayBuffer(4); + const view = new Buffer(storage); + view[1] = 5; + console.log(new Uint8Array(storage), view.buffer === storage); + const text: string = "typed"; + console.log(new Buffer(text).toString("hex")); + try { + sized(-2); + } catch (e) { + console.log((e as Error).name, (e as { code?: string }).code); + } + console.log("sync end"); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1]!, "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7531-buffer-constructor-dependency/main.cjs b/tests/corpus/7531-buffer-constructor-dependency/main.cjs new file mode 100644 index 000000000..75b130bc6 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/main.cjs @@ -0,0 +1,36 @@ +// The deprecated Buffer constructor called from a dependency under +// node_modules: Node reports no DEP0005 for those calls, and the program's +// own first call reports it once, on the next tick. +// The program re-runs itself and prints the child's output with process +// ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const legacy = require("legacy-buf"); + +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code)); + + console.log(legacy.construct("hi", "utf8"), legacy.construct(3), legacy.call([1, 2])); + const storage = new Uint8Array([1, 2, 3, 4]).buffer; + console.log(legacy.construct(storage, 1, 2), legacy.many(5)); + try { + legacy.construct(4, "hex"); + } catch (e) { + console.log(e.code); + } + process.nextTick(() => { + console.log("tick"); + console.log(new Buffer("own")); + console.log(Buffer(2), new Buffer([9])); + setTimeout(() => console.log("timer"), 0); + }); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts new file mode 100644 index 000000000..ef314d625 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts @@ -0,0 +1,3 @@ +export declare function construct(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer; +export declare function call(value: unknown): Buffer; +export declare function many(count: number): number; diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js new file mode 100644 index 000000000..062e4e41d --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A dependency that keeps the deprecated Buffer constructor. +exports.construct = function (value, encodingOrOffset, length) { + if (length !== undefined) return new Buffer(value, encodingOrOffset, length); + if (encodingOrOffset !== undefined) return new Buffer(value, encodingOrOffset); + return new Buffer(value); +}; +exports.call = function (value) { + return Buffer(value); +}; +exports.many = function (count) { + let total = 0; + for (let i = 0; i < count; i++) total += new Buffer(1).length; + return total; +}; diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json new file mode 100644 index 000000000..db2277b60 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json @@ -0,0 +1,5 @@ +{ + "name": "legacy-buf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/corpus/7532-buffer-constructor-check-limit/main.cjs b/tests/corpus/7532-buffer-constructor-check-limit/main.cjs new file mode 100644 index 000000000..c08690a83 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/main.cjs @@ -0,0 +1,10 @@ +// Node checks the caller of the deprecated Buffer constructor for its first +// 10000 calls only: once a dependency under node_modules has made them, the +// program's own calls report no DEP0005. +"use strict"; +process.on("warning", (w) => console.log("listener", w.code)); +const legacy = require("legacy-buf"); + +console.log(legacy.many(10000)); +console.log(new Buffer("after"), Buffer(1)); +setTimeout(() => console.log("timer"), 0); diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts new file mode 100644 index 000000000..ef314d625 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts @@ -0,0 +1,3 @@ +export declare function construct(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer; +export declare function call(value: unknown): Buffer; +export declare function many(count: number): number; diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js new file mode 100644 index 000000000..062e4e41d --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A dependency that keeps the deprecated Buffer constructor. +exports.construct = function (value, encodingOrOffset, length) { + if (length !== undefined) return new Buffer(value, encodingOrOffset, length); + if (encodingOrOffset !== undefined) return new Buffer(value, encodingOrOffset); + return new Buffer(value); +}; +exports.call = function (value) { + return Buffer(value); +}; +exports.many = function (count) { + let total = 0; + for (let i = 0; i < count; i++) total += new Buffer(1).length; + return total; +}; diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json new file mode 100644 index 000000000..db2277b60 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json @@ -0,0 +1,5 @@ +{ + "name": "legacy-buf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/corpus/7533-buffer-value.cjs b/tests/corpus/7533-buffer-value.cjs new file mode 100644 index 000000000..3fd500ec2 --- /dev/null +++ b/tests/corpus/7533-buffer-value.cjs @@ -0,0 +1,101 @@ +// The Buffer function read as a value: one identity across the global, the +// buffer module and its aliases, Node's own enumerable members in Node's +// order, statics that apply Node's argument handling, instanceof, and a +// poolSize property the pool reads. +"use strict"; +const buffer = require("buffer"); +const nodeBuffer = require("node:buffer"); +const util = require("util"); + +// bn.js keeps the module's Buffer in a variable assigned under try. +var BnBuffer; +try { + BnBuffer = require("buffer").Buffer; +} catch (e) {} +const pass = (value) => value; +const Stored = pass(require("buffer").Buffer); +const Alias = Buffer; +const table = { Buffer: buffer.Buffer }; + +console.log(typeof Stored, Stored.name, Stored.length, Alias.name, Alias.length); +console.log(Stored === Buffer, Stored === globalThis.Buffer, Stored === buffer.Buffer, Stored === nodeBuffer.Buffer); +console.log(Stored === Alias, table.Buffer === Stored, Stored.constructor === Function); +console.log(Stored); +console.log(Object.keys(Stored)); +console.log(util.inspect({ nested: { Buffer: Stored } }, { depth: 1 })); + +// Statics through the value. +const show = (label, run) => { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +}; +show("from string", () => Stored.from("héllo")); +show("from hex", () => Stored.from("6869", "hex")); +show("from empty encoding", () => Stored.from("x", "")); +show("from bad encoding", () => Stored.from("x", "nope")); +show("from array", () => Stored.from([1, 2, 300])); +const storage = new Uint8Array([1, 2, 3, 4, 5]).buffer; +show("from range", () => Stored.from(storage, 1, 3)); +show("from number", () => Stored.from(5)); +show("alloc", () => Stored.alloc(3, "ab")); +show("alloc fill", () => Stored.alloc(2, 7)); +show("alloc string size", () => Stored.alloc("3")); +show("allocUnsafe", () => Stored.allocUnsafe(4).fill(9)); +show("allocUnsafe negative", () => Stored.allocUnsafe(-1)); +show("allocUnsafe string", () => Stored.allocUnsafe("4")); +show("allocUnsafeSlow", () => Stored.allocUnsafeSlow(2).fill(1)); +show("isBuffer", () => [Stored.isBuffer(Buffer.alloc(1)), Stored.isBuffer(new Uint8Array(1)), Stored.isBuffer("x")]); +show("isEncoding", () => [Stored.isEncoding("UTF-8"), Stored.isEncoding(""), Stored.isEncoding("nope"), Stored.isEncoding(1)]); +show("compare", () => Stored.compare(Buffer.from("a"), Buffer.from("b"))); +show("compare bad", () => Stored.compare("a", Buffer.from("b"))); +show("concat", () => Stored.concat([Buffer.from("ab"), Buffer.from("cd")])); +show("concat length", () => Stored.concat([Buffer.from("ab"), Buffer.from("cd")], 3)); +show("concat empty", () => Stored.concat([], "x")); +show("concat bad length", () => Stored.concat([Buffer.from("a")], "x")); +show("concat bad list", () => Stored.concat("x")); +show("byteLength", () => [Stored.byteLength("héllo"), Stored.byteLength("aGk=", "base64"), Stored.byteLength(new Uint16Array(3))]); + +// Member values keep one identity, with Node's names and lengths. +const members = [ + [Buffer.from, Stored.from], + [Buffer.alloc, Stored.alloc], + [Buffer.allocUnsafe, Stored.allocUnsafe], + [Buffer.isBuffer, Stored.isBuffer], + [Buffer.compare, Stored.compare], + [Buffer.concat, Stored.concat], + [Buffer.byteLength, Stored.byteLength], + [Buffer.isEncoding, Stored.isEncoding], +]; +console.log(members.map(([a, b]) => `${a.name}/${a.length}/${a === b}`).join(" ")); +const isBuffer = require("buffer").Buffer.isBuffer; +console.log(isBuffer(Buffer.from("x")), isBuffer(null), typeof Buffer.allocUnsafe); + +// A bn.js-style allocator over Buffer or Array. +function allocate(ArrayType, size) { + if (ArrayType.allocUnsafe) return ArrayType.allocUnsafe(size); + return new ArrayType(size); +} +function toArrayLike(ArrayType, size) { + const made = allocate(ArrayType, size); + made.fill(4); + return made; +} +function toBuffer(size) { + return BnBuffer ? toArrayLike(BnBuffer, size) : null; +} +const made = toBuffer(3); +console.log(made, toArrayLike(Array, 2), made instanceof Stored, made instanceof Buffer); +console.log(new Uint8Array(2) instanceof Stored, Buffer.from("x") instanceof Alias, pass("x") instanceof Stored); + +// poolSize: one setting through the value and the static spelling. +console.log(Stored.poolSize, Buffer.poolSize); +Buffer.poolSize = 1024; +console.log(Stored.poolSize); +const small = Buffer.allocUnsafe(600); +console.log(small.buffer.byteLength, Buffer.allocUnsafe(400).buffer.byteLength); +Stored.poolSize = 2048; +console.log(Buffer.poolSize, Buffer.allocUnsafe(600).buffer.byteLength); +Stored.poolSize = 65536; diff --git a/tests/corpus/7534-buffer-prototype-members.cjs b/tests/corpus/7534-buffer-prototype-members.cjs new file mode 100644 index 000000000..2fd347373 --- /dev/null +++ b/tests/corpus/7534-buffer-prototype-members.cjs @@ -0,0 +1,29 @@ +// Buffer.prototype members read as values, as smart-buffer probes and +// calls them: own methods are functions with Node's names and lengths that +// run on a Buffer receiver, the parent and offset getters read on the +// prototype answer undefined, constructor is Buffer, and absent names are +// undefined. +"use strict"; +const buffer_1 = require("buffer"); + +function check(method) { + if (typeof buffer_1.Buffer.prototype[method] === "undefined") return `${method}: missing`; + return `${method}: ${typeof buffer_1.Buffer.prototype[method]}`; +} +console.log(["writeBigInt64BE", "readBigUInt64LE", "readUInt8", "nope", "parent", "offset"].map(check)); +console.log(Buffer.prototype.constructor === Buffer, Buffer.prototype["constructor"] === buffer_1.Buffer); +console.log(Buffer.prototype[0], Buffer.prototype[undefined], Buffer.prototype.toJSON === Buffer.prototype.toJSON); + +const names = ["readUInt8", "readUint8", "readFloatLE", "writeUInt16LE", "readUIntLE", "toString", "toLocaleString", "slice", "hexSlice"]; +console.log(names.map((n) => `${n}=${JSON.stringify(Buffer.prototype[n].name)}/${Buffer.prototype[n].length}`).join(" ")); + +const buf = Buffer.alloc(8); +function write(func, value, offset) { + return func.call(buf, value, offset); +} +console.log(write(Buffer.prototype.writeUInt16LE, 513, 0), write(Buffer.prototype.writeInt8, -2, 2), buf); +console.log(Buffer.prototype.readUInt16LE.call(buf, 0), Buffer.prototype.readInt8.call(buf, 2)); +console.log(Buffer.prototype.toString.call(buf, "hex", 0, 3), Buffer.prototype.toLocaleString.call(Buffer.from("hi"))); +console.log(Buffer.prototype.slice.call(buf, 0, 2), Buffer.prototype.equals.call(buf, Buffer.alloc(8))); +console.log(Buffer.prototype.fill.call(Buffer.alloc(3), 1), Buffer.prototype.indexOf.call(Buffer.from("abc"), "c")); +console.log(Buffer.prototype.write.call(buf, "zz", 4), buf, Buffer.prototype.toJSON.call(Buffer.from([1]))); diff --git a/tests/corpus/7535-inspect-function-properties.cjs b/tests/corpus/7535-inspect-function-properties.cjs new file mode 100644 index 000000000..5e3f8c6a6 --- /dev/null +++ b/tests/corpus/7535-inspect-function-properties.cjs @@ -0,0 +1,29 @@ +// util.inspect of functions with own enumerable properties, as Node's +// formatRaw prints them: the function's base then its keys (symbols +// included), `[Function]` past the depth limit, and the plain base when +// every own property is hidden. +"use strict"; +const util = require("util"); + +function named() {} +named.x = 1; +const arrow = () => {}; +arrow.a = "s"; +arrow[Symbol("k")] = 2; +Object.defineProperty(arrow, "hidden", { value: 3, enumerable: false }); +function hiddenOnly() {} +Object.defineProperty(hiddenOnly, "secret", { value: 1, enumerable: false }); +function big() {} +for (let i = 0; i < 8; i++) big["key" + i] = i; +function self() {} +self.me = self; + +const pass = (value) => value; + +console.log(named, arrow, hiddenOnly); +console.log(util.inspect(named, { depth: 0 })); +console.log(util.inspect(pass({ wrap: named }), { depth: 0 })); +console.log(util.inspect(pass([named, arrow]))); +console.log(util.inspect(big)); +console.log(util.inspect(self)); +console.log(util.inspect(pass({ a: { b: { f: big } } }))); diff --git a/tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json b/tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json new file mode 100644 index 000000000..593c181c3 --- /dev/null +++ b/tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json @@ -0,0 +1 @@ +{ "plain": 4043, "san": 6242 } diff --git a/tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json b/tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json new file mode 100644 index 000000000..759dce51e --- /dev/null +++ b/tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json @@ -0,0 +1 @@ +{ "plain": 3823, "san": 5864 } diff --git a/tests/harness/corpus-costs/7529-buffer-constructor.cjs.json b/tests/harness/corpus-costs/7529-buffer-constructor.cjs.json new file mode 100644 index 000000000..408808da4 --- /dev/null +++ b/tests/harness/corpus-costs/7529-buffer-constructor.cjs.json @@ -0,0 +1 @@ +{ "plain": 5309, "san": 56270 } diff --git a/tests/harness/corpus-costs/7530-buffer-constructor.ts.json b/tests/harness/corpus-costs/7530-buffer-constructor.ts.json new file mode 100644 index 000000000..287d50fdc --- /dev/null +++ b/tests/harness/corpus-costs/7530-buffer-constructor.ts.json @@ -0,0 +1 @@ +{ "plain": 4282, "san": 5688 } diff --git a/tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json b/tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json new file mode 100644 index 000000000..568483d98 --- /dev/null +++ b/tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 5222, "san": 6614 } diff --git a/tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json b/tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json new file mode 100644 index 000000000..3d262fe41 --- /dev/null +++ b/tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 4707, "san": 5678 } diff --git a/tests/harness/corpus-costs/7533-buffer-value.cjs.json b/tests/harness/corpus-costs/7533-buffer-value.cjs.json new file mode 100644 index 000000000..126c82265 --- /dev/null +++ b/tests/harness/corpus-costs/7533-buffer-value.cjs.json @@ -0,0 +1 @@ +{ "plain": 4874, "san": 10466 } diff --git a/tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json b/tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json new file mode 100644 index 000000000..624b7f12f --- /dev/null +++ b/tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json @@ -0,0 +1 @@ +{ "plain": 3944, "san": 5992 } diff --git a/tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json b/tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json new file mode 100644 index 000000000..58c48c344 --- /dev/null +++ b/tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json @@ -0,0 +1 @@ +{ "plain": 3458, "san": 4785 } diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index 905f648dd..7f86c0dc5 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -148,9 +148,9 @@ function nodeOracleFile(file: string): string { } /** `// @no-deprecation` in the directive head: the Node oracle runs with - * --no-deprecation — for programs exercising deprecated-API ERROR paths - * (new Buffer's ctor ladder) whose DEP warnings carry a pid and can never - * byte-compare; the compiled runtime emits no process warnings at all. */ + * --no-deprecation, for programs whose DEP warnings carry a pid and cannot + * compare. The compiled program does not read the directive; a program + * whose compiled form reports the warning sets process.noDeprecation. */ function wantsNoDeprecation(file: string): boolean { return directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l)); } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 319e7329f..39ecd9e79 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -2050,3 +2050,52 @@ console.log(words.length); expect(r.stdout).toBe(`${fence}${fence}1\n`); }); }); + +// The Buffer function read as a value models Node's statics; the forms it +// cannot answer as Node does throw SC2020: calling or constructing it +// through the value (Node reports DEP0005 by the caller's location), its +// prototype as a whole, copyBytesFrom and of, and Buffer.prototype members +// that are inherited, symbol-keyed or called on other receivers. +describe("Buffer value refusals", () => { + test("unmodeled Buffer value forms throw SC2020", async () => { + const r = await compileAndRun( + "buffer-value-refusals", + `const pass = (value) => value; +const Stored = pass(require("buffer").Buffer); +const runs = [ + () => Stored("x"), + () => new Stored(2), + () => Stored.prototype, + () => Stored.copyBytesFrom(new Uint8Array(2)), + () => Stored.of(1), + () => Buffer.prototype.map, + () => Buffer.prototype[Symbol.iterator], + () => Buffer.prototype.readUInt8.call(new Uint8Array(2), 0), +]; +for (const run of runs) { + try { + run(); + console.log("ran"); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} +`, + "cjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + [ + "Error SC2020 Calling or constructing Buffer through a stored Buffer value is not supported yet", + "Error SC2020 Calling or constructing Buffer through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.prototype through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.copyBytesFrom through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.of through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.prototype.map is not supported yet", + "Error SC2020 Buffer.prototype members keyed by symbols are not supported yet", + "Error SC2020 Buffer.prototype.readUInt8 on a receiver that is not a Buffer is not supported yet", + "", + ].join("\n"), + ); + }); +}); From 91e15f34267365c7464c61b4ff42a2e67dce2c77 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:40:52 -0500 Subject: [PATCH 13/45] Run child_process and http2 events in Node's AsyncLocalStorage contexts child_process and http2 events ran with no store, or with the context of whichever socket dispatch delivered them. Node runs them in the context of the handle or request that produces them. child_process: a child, its piped stdout, stderr and stdin, and its IPC channel keep the context of the spawn() or fork() call. 'spawn', 'error', 'exit', 'close', stdout's and stderr's 'data' and 'end', and the channel's 'message' and 'disconnect' run in it, in the POSIX and the Windows implementations. stdin's 'drain' and 'error' run in the context of the latest write() call, its 'finish' in the context of its end() call, and a send() callback in the context of its send() call. http2: a session keeps the context it was created in, the http2.connect() call for a client and the server's listen() context for a server session, and the session's socket callbacks and deferred emits run in it. A client stream keeps the context of its request() call, and the frames that drive a stream's events (DATA, WINDOW_UPDATE, RST_STREAM, and a client's HEADERS) and its deferred emits run in the stream's context. A session remembers the stream its last lookup by id found, so the context lookup and the frame's own handling share one search. A server stream and its compatibility request and response run with no store, as Node's do. An accepted h2c socket's own events run with no store, as an accepted net socket's do, so a server.close() that waits for its sessions calls back with no store; a TLS server's sockets keep the listen() context, as before. Corpus 7385 to 7389. --- .../builtin-apis/asynclocalstorage.mdx | 4 +- .../generated/node-v24-internal.json | 61 ++++- internal/compatibility/static-support.json | 2 +- .../7385-als-child-process-events.ts.json | 6 + .../7386-als-child-fork-events/main.mjs.json | 6 + .../corpus/7387-als-http2-events.mjs.json | 6 + .../7388-als-http2-compat-events.mjs.json | 6 + .../7389-als-http2-secure-events.mjs.json | 6 + packages/runtime/src/scr_child.c | 221 +++++++++++++++++- packages/runtime/src/scr_http2.c | 95 +++++++- tests/corpus/7385-als-child-process-events.ts | 104 +++++++++ .../7386-als-child-fork-events/main.mjs | 25 ++ .../7386-als-child-fork-events/worker.mjs | 4 + tests/corpus/7387-als-http2-events.mjs | 91 ++++++++ tests/corpus/7388-als-http2-compat-events.mjs | 49 ++++ tests/corpus/7389-als-http2-secure-events.mjs | 94 ++++++++ .../7385-als-child-process-events.ts.json | 1 + .../7386-als-child-fork-events/main.mjs.json | 1 + .../7387-als-http2-events.mjs.json | 1 + .../7388-als-http2-compat-events.mjs.json | 1 + .../7389-als-http2-secure-events.mjs.json | 1 + 21 files changed, 766 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json create mode 100644 tests/corpus/7385-als-child-process-events.ts create mode 100644 tests/corpus/7386-als-child-fork-events/main.mjs create mode 100644 tests/corpus/7386-als-child-fork-events/worker.mjs create mode 100644 tests/corpus/7387-als-http2-events.mjs create mode 100644 tests/corpus/7388-als-http2-compat-events.mjs create mode 100644 tests/corpus/7389-als-http2-secure-events.mjs create mode 100644 tests/harness/corpus-costs/7385-als-child-process-events.ts.json create mode 100644 tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json create mode 100644 tests/harness/corpus-costs/7387-als-http2-events.mjs.json create mode 100644 tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json create mode 100644 tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 016b712f6..ce77d57f5 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -6,5 +6,7 @@ Stream events and callbacks run with the stores of the code that triggers them, as in Node. Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call. `fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them. - Events of `child_process` children and of `http2` sessions and streams do not yet run in the contexts Node gives them. + Events of `child_process` children, their standard streams and their IPC channels see the context of the `spawn()` or `fork()` call; stdin's `'finish'` and `send()` callbacks see the context of their call. + A client `http2` session's events see the context of its `http2.connect()` call, and a client stream's events the context of its `request()` call. + An `http2` server's `'session'` and `'stream'` events and the compatibility API's request handler see the context of its `listen()` call; a server stream's own events, and its compatibility request's and response's, run with no store, as in Node. `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index b5542944d..58a707f92 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1394,7 +1394,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "chapter" @@ -1451,7 +1456,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1531,7 +1541,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1565,7 +1580,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1599,7 +1619,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1633,7 +1658,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1667,7 +1697,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1978,6 +2013,11 @@ "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2547,7 +2587,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 640345a02..a711bcf3c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json new file mode 100644 index 000000000..aac67defb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7385-als-child-process-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json new file mode 100644 index 000000000..a334cf0aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7386-als-child-fork-events/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json new file mode 100644 index 000000000..272024d1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7387-als-http2-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json new file mode 100644 index 000000000..8a64f395e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7388-als-http2-compat-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json new file mode 100644 index 000000000..aec8e2d2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7389-als-http2-secure-events.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656d..19633f8dd 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -1404,6 +1404,10 @@ struct ScrChildStream { size_t n_data, cap_data; ScrChildStreamEndL *end_ls; size_t n_end, cap_end; + /* The AsyncLocalStorage context of the spawn() call that created the + * stream (+1, NULL outside a store): its events run in it, as Node's + * pipe handles run theirs. */ + ScrAlsCtx *als; struct ScrChildStream *next; /* the service registry (+1) */ }; @@ -1432,6 +1436,11 @@ struct ScrChildWriter { size_t n_finish, cap_finish; ScrChildErrEntry *err_ls; size_t n_write_err, cap_write_err; + /* AsyncLocalStorage contexts (+1 each, NULL outside a store): the + * latest write() call's, for 'drain' and 'error' (the spawn() call's + * until the first write), and the end() call's, for 'finish'. */ + ScrAlsCtx *als; + ScrAlsCtx *end_als; struct ScrChildWriter *next; /* the service registry (+1) */ }; @@ -1478,6 +1487,9 @@ struct ScrChild { ScrChildWriter *in_stream; ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; + /* The AsyncLocalStorage context of the spawn() call (+1, NULL outside + * a store): 'spawn', 'error', 'exit' and 'close' run in it. */ + ScrAlsCtx *als; struct ScrChild *next; /* the pending registry */ }; @@ -1536,6 +1548,7 @@ void scr_child_release(ScrChild *c) { scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); scr_ipc_release(c->ipc); + scr_als_ctx_release(c->als); if (c->proc != NULL) CloseHandle(c->proc); free(c); } @@ -1568,6 +1581,7 @@ void scr_child_stream_release(ScrChildStream *s) { if (s->h != NULL) CloseHandle(s->h); scr_str_release(s->encoding); free(s->capture_data); + scr_als_ctx_release(s->als); free(s); } } @@ -1587,6 +1601,7 @@ static ScrChildStream *scr_child_stream_new(HANDLE h) { ScrChildStream *s = calloc(1, sizeof *s); if (!s) scr_child_oom(); s->rc = 1; + s->als = scr_als_capture(); s->h = h; if (h == NULL) { s->eof = true; @@ -1633,8 +1648,16 @@ static ScrStr *scr_child_stream_capture_value(ScrChildStream *s) { : scr_str_new((const char *)s->capture_data, s->capture_len); } -static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, - ScrStr *text) { +static void scr_child_stream_emit_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text); + +/* 'data' runs in the spawn() call's context. */ +static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_emit_in(s, bytes, text); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_emit_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { size_t nd = s->n_data; if (nd == 0) return; ScrChildStreamDataL *snap = malloc(nd * sizeof *snap); @@ -1676,7 +1699,16 @@ static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, free(snap); } +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end); + +/* A stream's 'end' runs in the spawn() call's context. */ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_finish_in(s, fire_end); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end) { if (fire_end && s->encoding != NULL && s->n_data > 0) { ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); s->dec_pending = 0; @@ -1907,6 +1939,8 @@ void scr_child_writer_release(ScrChildWriter *w) { free(w->buf); scr_str_release(w->err_msg); scr_child_writer_drop_listeners(w); + scr_als_ctx_release(w->als); + scr_als_ctx_release(w->end_als); free(w); } } @@ -1922,6 +1956,7 @@ static ScrChildWriter *scr_child_writer_new(HANDLE h) { ScrChildWriter *w = calloc(1, sizeof *w); if (!w) scr_child_oom(); w->rc = 1; + w->als = scr_als_capture(); w->h = h; w->writable = h != NULL; w->destroyed = h == NULL; @@ -2068,6 +2103,8 @@ static void scr_child_writer_pump(ScrChildWriter *w) { static bool scr_child_writer_write(ScrChildWriter *w, const void *data, size_t len) { + scr_als_ctx_release(w->als); + w->als = scr_als_capture(); if (!w->writable) { scr_child_writer_fail(w, "write after end", "ERR_STREAM_WRITE_AFTER_END"); return false; @@ -2098,6 +2135,8 @@ bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) { void scr_child_writer_end(ScrChildWriter *w) { if (w->ending || w->finish_pending || w->finished || w->destroyed) return; + scr_als_ctx_release(w->end_als); + w->end_als = scr_als_capture(); w->writable = false; w->ending = true; scr_child_writer_pump(w); @@ -2165,7 +2204,16 @@ void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/, w->err_ls[w->n_write_err++] = (ScrChildErrEntry){cb, fn}; } +static void scr_child_writer_fire_drain_in(ScrChildWriter *w); + +/* 'drain' runs in the latest write() call's context. */ static void scr_child_writer_fire_drain(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_drain_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_drain_in(ScrChildWriter *w) { w->drain_pending = false; size_t n = w->n_drain; if (n == 0) return; @@ -2192,7 +2240,16 @@ static void scr_child_writer_fire_drain(ScrChildWriter *w) { free(snap); } +static void scr_child_writer_fire_finish_in(ScrChildWriter *w); + +/* 'finish' runs in the end() call's context. */ static void scr_child_writer_fire_finish(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->end_als); + scr_child_writer_fire_finish_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_finish_in(ScrChildWriter *w) { w->finish_pending = false; w->finished = true; w->destroyed = true; @@ -2211,7 +2268,16 @@ static void scr_child_writer_fire_finish(ScrChildWriter *w) { scr_child_writer_drop_zero_listeners(w); } +static void scr_child_writer_fire_error_in(ScrChildWriter *w); + +/* 'error' runs in the latest write() call's context. */ static void scr_child_writer_fire_error(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_error_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_error_in(ScrChildWriter *w) { w->error_pending = false; if (w->n_write_err == 0) { fflush(stdout); @@ -2305,6 +2371,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, ScrChild *c = calloc(1, sizeof(ScrChild)); if (!c) scr_child_oom(); c->rc = 1; + c->als = scr_als_capture(); /* Piped slots: the pipe NOW; a pipe failure degrades the slot to NUL * and the stream husk answers eof (fd exhaustion — nothing real). */ @@ -2703,7 +2770,16 @@ static bool scr_child_close_ready(const ScrChild *c) { scr_ipc_close_ready(c->ipc); } +static void scr_child_fire_close_in(ScrChild *c); + +/* 'close' runs in the spawn() call's context. */ static void scr_child_fire_close(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_close_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_close_in(ScrChild *c) { c->close_done = true; bool has_code = c->state == SCR_CHILD_SPAWN_FAILED ? true : c->has_code; double code = c->state == SCR_CHILD_SPAWN_FAILED @@ -2767,7 +2843,16 @@ bool scr_children_wait(double max_wait_ms) { /* Fires one settled child's terminal event — the POSIX arm's settle, * verbatim (streams drain first so 'end' precedes 'exit'; spawn * failures fire 'error' or die unhandled). */ +static void scr_child_settle_in(ScrChild *c); + +/* A child's terminal events run in its spawn() call's context. */ static void scr_child_settle(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_settle_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_settle_in(ScrChild *c) { if (c->reffed) { c->reffed = false; scr_children_reffed_n--; @@ -3815,6 +3900,10 @@ struct ScrChildStream { size_t n_data, cap_data; ScrChildStreamEndL *end_ls; size_t n_end, cap_end; + /* The AsyncLocalStorage context of the spawn() call that created the + * stream (+1, NULL outside a store): its events run in it, as Node's + * pipe handles run theirs. */ + ScrAlsCtx *als; struct ScrChildStream *next; /* the service registry (+1) */ }; @@ -3839,6 +3928,11 @@ struct ScrChildWriter { size_t n_finish, cap_finish; ScrChildErrEntry *err_ls; size_t n_write_err, cap_write_err; + /* AsyncLocalStorage contexts (+1 each, NULL outside a store): the + * latest write() call's, for 'drain' and 'error' (the spawn() call's + * until the first write), and the end() call's, for 'finish'. */ + ScrAlsCtx *als; + ScrAlsCtx *end_als; struct ScrChildWriter *next; /* the service registry (+1) */ }; @@ -3889,6 +3983,9 @@ struct ScrChild { ScrChildWriter *in_stream; ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; + /* The AsyncLocalStorage context of the spawn() call (+1, NULL outside + * a store): 'spawn', 'error', 'exit' and 'close' run in it. */ + ScrAlsCtx *als; struct ScrChild *next; /* the pending registry */ }; @@ -4085,6 +4182,7 @@ void scr_child_release(ScrChild *c) { scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); scr_ipc_release(c->ipc); + scr_als_ctx_release(c->als); free(c); } } @@ -4137,6 +4235,7 @@ void scr_child_stream_release(ScrChildStream *s) { if (s->fd >= 0) close(s->fd); scr_str_release(s->encoding); free(s->capture_data); + scr_als_ctx_release(s->als); free(s); } } @@ -4159,6 +4258,7 @@ static ScrChildStream *scr_child_stream_new(int fd) { ScrChildStream *s = calloc(1, sizeof *s); if (!s) scr_child_oom(); s->rc = 1; + s->als = scr_als_capture(); s->fd = fd; if (fd < 0) { s->eof = true; @@ -4213,8 +4313,16 @@ static ScrStr *scr_child_stream_capture_value(ScrChildStream *s) { : scr_str_new((const char *)s->capture_data, s->capture_len); } -static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, - ScrStr *text) { +static void scr_child_stream_emit_posix_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text); + +/* 'data' runs in the spawn() call's context. */ +static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_emit_posix_in(s, bytes, text); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_emit_posix_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { size_t nd = s->n_data; if (nd == 0) return; ScrChildStreamDataL *snap = malloc(nd * sizeof *snap); @@ -4264,7 +4372,16 @@ static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, /* EOF/error tail: 'end' fires over a snapshot, everything drops, the fd * closes (deleting its kqueue filter), and the liveness/fallback counters * settle. The caller unlinks from the registry and releases its ref. */ +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end); + +/* A stream's 'end' runs in the spawn() call's context. */ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_finish_in(s, fire_end); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end) { if (fire_end && s->encoding != NULL && s->n_data > 0) { ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); s->dec_pending = 0; @@ -4572,6 +4689,8 @@ void scr_child_writer_release(ScrChildWriter *w) { free(w->buf); scr_str_release(w->err_msg); scr_child_writer_drop_listeners(w); + scr_als_ctx_release(w->als); + scr_als_ctx_release(w->end_als); free(w); } } @@ -4587,6 +4706,7 @@ static ScrChildWriter *scr_child_writer_new(int fd) { ScrChildWriter *w = calloc(1, sizeof *w); if (!w) scr_child_oom(); w->rc = 1; + w->als = scr_als_capture(); w->fd = fd; w->writable = fd >= 0; w->destroyed = fd < 0; @@ -4720,6 +4840,8 @@ static void scr_child_writer_pump(ScrChildWriter *w) { static bool scr_child_writer_write(ScrChildWriter *w, const void *data, size_t len) { + scr_als_ctx_release(w->als); + w->als = scr_als_capture(); if (!w->writable) { scr_child_writer_fail(w, "write after end", "ERR_STREAM_WRITE_AFTER_END"); return false; @@ -4756,6 +4878,8 @@ bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) { void scr_child_writer_end(ScrChildWriter *w) { if (w->ending || w->finish_pending || w->finished || w->destroyed) return; + scr_als_ctx_release(w->end_als); + w->end_als = scr_als_capture(); w->writable = false; w->ending = true; scr_child_writer_pump(w); @@ -4823,7 +4947,16 @@ void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/, w->err_ls[w->n_write_err++] = (ScrChildErrEntry){cb, fn}; } +static void scr_child_writer_fire_drain_in(ScrChildWriter *w); + +/* 'drain' runs in the latest write() call's context. */ static void scr_child_writer_fire_drain(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_drain_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_drain_in(ScrChildWriter *w) { w->drain_pending = false; size_t n = w->n_drain; if (n == 0) return; @@ -4850,7 +4983,16 @@ static void scr_child_writer_fire_drain(ScrChildWriter *w) { free(snap); } +static void scr_child_writer_fire_finish_in(ScrChildWriter *w); + +/* 'finish' runs in the end() call's context. */ static void scr_child_writer_fire_finish(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->end_als); + scr_child_writer_fire_finish_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_finish_in(ScrChildWriter *w) { w->finish_pending = false; w->finished = true; w->destroyed = true; @@ -4869,7 +5011,16 @@ static void scr_child_writer_fire_finish(ScrChildWriter *w) { scr_child_writer_drop_zero_listeners(w); } +static void scr_child_writer_fire_error_in(ScrChildWriter *w); + +/* 'error' runs in the latest write() call's context. */ static void scr_child_writer_fire_error(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_error_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_error_in(ScrChildWriter *w) { w->error_pending = false; if (w->n_write_err == 0) { fflush(stdout); @@ -4973,6 +5124,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, ScrChild *c = calloc(1, sizeof(ScrChild)); if (!c) scr_child_oom(); c->rc = 1; + c->als = scr_als_capture(); /* Piped slots: pipe(2) NOW, both ends cloexec (the file action's dup2 * clears the flag on the child's copy — every other descriptor closes @@ -5489,7 +5641,16 @@ static bool scr_child_close_ready(const ScrChild *c) { scr_ipc_close_ready(c->ipc); } +static void scr_child_fire_close_in(ScrChild *c); + +/* 'close' runs in the spawn() call's context. */ static void scr_child_fire_close(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_close_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_close_in(ScrChild *c) { c->close_done = true; bool has_code = c->state == SCR_CHILD_SPAWN_FAILED ? true : c->has_code; double code = c->state == SCR_CHILD_SPAWN_FAILED @@ -5527,7 +5688,16 @@ static void scr_children_closing_poll(void) { * ordering runs first: consumer-owning piped streams drain to EOF/EAGAIN * so their 'end' events fire BEFORE 'exit' (Node-exact — verified: data*, * end, then exit for every output shape). */ +static void scr_child_settle_in(ScrChild *c); + +/* A child's terminal events run in its spawn() call's context. */ static void scr_child_settle(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_settle_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_settle_in(ScrChild *c) { if (c->reffed) { c->reffed = false; scr_children_reffed_n--; @@ -5695,7 +5865,16 @@ void scr_child_on_spawn(ScrChild *c, ScrClosure *cb) { /* Success is observable on the next loop pass, before any data or exit * event. Detaching the snapshot keeps listeners registered by a callback * from participating in an event that has already started. */ +static void scr_child_fire_spawn_in(ScrChild *c); + +/* 'spawn' runs in the spawn() call's context. */ static void scr_child_fire_spawn(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_spawn_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_spawn_in(ScrChild *c) { c->spawn_done = true; ScrClosure **callbacks = c->spawn_cbs; size_t count = c->n_spawn; @@ -5901,6 +6080,7 @@ typedef struct { typedef struct { ScrClosure *cb; ScrIpcSendFn fn; + ScrAlsCtx *ctx; /* the send() call's context (+1): the callback runs in it */ } ScrIpcSendEntry; struct ScrIpc { @@ -5928,6 +6108,9 @@ struct ScrIpc { ScrIpcSendEntry *send_cbs; size_t n_send, cap_send; ScrStr *send_error; + /* The context the channel was created in (+1): 'message' and + * 'disconnect' run in it. */ + ScrAlsCtx *als; struct ScrIpc *next; }; @@ -5963,8 +6146,12 @@ static void scr_ipc_release(ScrIpc *ipc) { for (size_t i = 0; i < ipc->n_pending; i++) scr_dyn_release(ipc->pending[i]); for (size_t i = 0; i < ipc->n_message; i++) scr_closure_release(ipc->message_ls[i].cb); for (size_t i = 0; i < ipc->n_disconnect; i++) scr_closure_release(ipc->disconnect_ls[i].cb); - for (size_t i = 0; i < ipc->n_send; i++) scr_closure_release(ipc->send_cbs[i].cb); + for (size_t i = 0; i < ipc->n_send; i++) { + scr_closure_release(ipc->send_cbs[i].cb); + scr_als_ctx_release(ipc->send_cbs[i].ctx); + } scr_str_release(ipc->send_error); + scr_als_ctx_release(ipc->als); free(ipc->buffer); free(ipc->pending); free(ipc->message_ls); @@ -6084,6 +6271,7 @@ static ScrIpc *scr_ipc_new(ScrChildStream *reader, ScrChildWriter *writer) { ScrIpc *ipc = calloc(1, sizeof(*ipc)); if (!ipc) scr_trap("out of memory"); ipc->rc = 1; + ipc->als = scr_als_capture(); ipc->connected = true; ipc->reader = reader; ipc->writer = writer; @@ -6114,7 +6302,7 @@ static void scr_ipc_add_send(ScrIpc *ipc, ScrClosure *cb, ScrIpcSendFn fn) { ipc->send_cbs = realloc(ipc->send_cbs, ipc->cap_send * sizeof(*ipc->send_cbs)); if (!ipc->send_cbs) scr_trap("out of memory"); } - ipc->send_cbs[ipc->n_send++] = (ScrIpcSendEntry){cb, fn}; + ipc->send_cbs[ipc->n_send++] = (ScrIpcSendEntry){cb, fn, scr_als_capture()}; } static bool scr_ipc_send(ScrIpc *ipc, ScrStr *json, ScrClosure *cb, @@ -6198,7 +6386,16 @@ static void scr_ipc_remove_message_once(ScrIpc *ipc, ScrClosure *cb) { } } +static void scr_ipc_fire_messages_in(ScrIpc *ipc); + +/* 'message' runs in the channel's creation context. */ static void scr_ipc_fire_messages(ScrIpc *ipc) { + ScrAlsCtx *prev = scr_als_dispatch_enter(ipc->als); + scr_ipc_fire_messages_in(ipc); + scr_als_dispatch_leave(prev); +} + +static void scr_ipc_fire_messages_in(ScrIpc *ipc) { while (ipc->n_pending > 0 && ipc->n_message > 0 && !scr_exc_pending()) { ScrDyn *message = ipc->pending[0]; memmove(ipc->pending, ipc->pending + 1, @@ -6235,16 +6432,28 @@ static void scr_ipc_fire_send_callbacks(ScrIpc *ipc) { error = scr_error_new(0, ipc->send_error); scr_error_set_code(error, "ERR_IPC_CHANNEL_CLOSED"); } + ScrAlsCtx *prev = scr_als_dispatch_enter(snapshot[i].ctx); if (!scr_exc_pending()) snapshot[i].fn(snapshot[i].cb, error); else scr_error_release(error); + scr_als_dispatch_leave(prev); scr_closure_release(snapshot[i].cb); + scr_als_ctx_release(snapshot[i].ctx); } free(snapshot); scr_str_release(ipc->send_error); ipc->send_error = NULL; } +static void scr_ipc_fire_disconnect_in(ScrIpc *ipc); + +/* 'disconnect' runs in the channel's creation context. */ static void scr_ipc_fire_disconnect(ScrIpc *ipc) { + ScrAlsCtx *prev = scr_als_dispatch_enter(ipc->als); + scr_ipc_fire_disconnect_in(ipc); + scr_als_dispatch_leave(prev); +} + +static void scr_ipc_fire_disconnect_in(ScrIpc *ipc) { if (!ipc->disconnect_pending || ipc->disconnect_emitted) return; ipc->disconnect_pending = false; ipc->disconnect_emitted = true; diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c index 4c5267e6a..55d880c01 100644 --- a/packages/runtime/src/scr_http2.c +++ b/packages/runtime/src/scr_http2.c @@ -678,6 +678,10 @@ struct ScrH2Stream { int64_t send_window; ScrNetLs data_ls, end_ls, close_ls, err_ls, resp_ls, aborted_ls, ready_ls, trailers_ls, wanttrailers_ls, push_ls, frameerr_ls; + /* The AsyncLocalStorage context of the request() call that created a + * client stream (+1, NULL outside a store); NULL on server streams, + * whose own events Node runs with no store. */ + ScrAlsCtx *als; }; /* The server-side ctx (the ScrHttpSrvCtx shape; `proto` FIRST — the @@ -757,6 +761,14 @@ struct ScrH2Session { bool error_emitted; ScrNetLs stream_ls, close_ls, err_ls, connect_ls, goaway_ls, remote_settings_ls, local_settings_ls; + /* The stream the last lookup by id found (borrowed from `streams`): + * the frames of one stream arrive together, and a frame's context + * lookup and its handling ask for the same stream. */ + ScrH2Stream *last_found; + /* The AsyncLocalStorage context the session was created in (+1, NULL + * outside a store): the connect() call's for a client session, the + * server's listen() context for a server session. */ + ScrAlsCtx *als; }; /* ── refcounts + the RC audit ────────────────────────────────────────── */ @@ -838,6 +850,7 @@ static void scr_h2_stream_free(ScrH2Stream *st) { } free(st->wbuf.data); scr_http2_session_release(st->sess); + scr_als_ctx_release(st->als); free(st); #ifdef SCR_RC_AUDIT scr_h2_streams_live--; @@ -864,6 +877,7 @@ static void scr_h2_session_free(ScrH2Session *s) { scr_h2_dyntab_free(&s->dec_table); if (s->srv) scr_h2_srv_ctx_release(s->srv); if (s->sock) scr_net_sock_release(s->sock); + scr_als_ctx_release(s->als); free(s); #ifdef SCR_RC_AUDIT scr_h2_sessions_live--; @@ -1011,8 +1025,9 @@ static void scr_h2_send_headers(ScrH2Session *s, int32_t stream_id, const ScrH2B /* ── stream bookkeeping ──────────────────────────────────────────────── */ static ScrH2Stream *scr_h2_find_stream(ScrH2Session *s, int32_t id) { + if (s->last_found != NULL && s->last_found->id == id) return s->last_found; for (size_t i = 0; i < s->nstreams; i++) { - if (s->streams[i]->id == id) return s->streams[i]; + if (s->streams[i]->id == id) return s->last_found = s->streams[i]; } return NULL; } @@ -1023,6 +1038,7 @@ static ScrH2Stream *scr_h2_stream_new(ScrH2Session *sess, int32_t id) { st->rc = 1; st->id = id; st->sess = scr_http2_session_retain(sess); + if (sess->client) st->als = scr_als_capture(); st->send_window = sess->peer_init_window ? (int64_t)sess->peer_init_window : SCR_H2_DEFAULT_WINDOW; #ifdef SCR_RC_AUDIT scr_h2_streams_live++; @@ -1039,6 +1055,7 @@ static ScrH2Stream *scr_h2_stream_new(ScrH2Session *sess, int32_t id) { /* Drop the session's ref on a settled stream (its 'close' is queued or * emitted — the vector entry is what kept it discoverable by id). */ static void scr_h2_session_forget_stream(ScrH2Session *s, ScrH2Stream *st) { + if (s->last_found == st) s->last_found = NULL; for (size_t i = 0; i < s->nstreams; i++) { if (s->streams[i] == st) { memmove(s->streams + i, s->streams + i + 1, (s->nstreams - i - 1) * sizeof *s->streams); @@ -1150,6 +1167,10 @@ static bool scr_h2_proto_pending(void) { return scr_h2_emits_head < scr_h2_emits static void scr_h2_proto_sweep(void) { while (scr_h2_emits_head < scr_h2_emits_len) { ScrH2Emit e = scr_h2_emits[scr_h2_emits_head++]; + ScrAlsCtx *prev = scr_als_dispatch_enter( + e.kind == SCR_H2_EMIT_STREAM_CLOSE || e.kind == SCR_H2_EMIT_STREAM_DRAIN + ? ((ScrH2Stream *)e.h)->als + : ((ScrH2Session *)e.h)->als); switch (e.kind) { case SCR_H2_EMIT_STREAM_CLOSE: { ScrH2Stream *st = (ScrH2Stream *)e.h; @@ -1227,6 +1248,7 @@ static void scr_h2_proto_sweep(void) { break; } } + scr_als_dispatch_leave(prev); scr_h2_emit_release(&e); if (scr_exc_pending()) return; } @@ -1380,8 +1402,29 @@ static void scr_h2_fire_settings(ScrNetLs *ls, ScrH2Session *s, const ScrH2Setti } /* One complete frame. Returns false when the session tore down. */ +static bool scr_h2_on_frame_in(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, + const uint8_t *p, size_t n); + +/* A frame that drives an open stream's events (DATA, WINDOW_UPDATE, + * RST_STREAM, and a client's HEADERS, CONTINUATION and PUSH_PROMISE) runs + * in that stream's context; other frames, a server stream's opening + * HEADERS included, in the session's, which the socket callbacks below + * enter. */ static bool scr_h2_on_frame(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, const uint8_t *p, size_t n) { + bool stream_frame = type == SCR_H2_F_DATA || type == SCR_H2_F_WINDOW_UPDATE || + type == SCR_H2_F_RST_STREAM || + (s->client && (type == SCR_H2_F_HEADERS || type == SCR_H2_F_CONTINUATION || + type == SCR_H2_F_PUSH_PROMISE)); + ScrH2Stream *st = stream_frame && stream_id != 0 ? scr_h2_find_stream(s, stream_id) : NULL; + ScrAlsCtx *prev = st != NULL ? scr_als_dispatch_enter(st->als) : NULL; + bool ok = scr_h2_on_frame_in(s, type, flags, stream_id, p, n); + if (st != NULL) scr_als_dispatch_leave(prev); + return ok; +} + +static bool scr_h2_on_frame_in(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, + const uint8_t *p, size_t n) { /* An open header block admits only its CONTINUATIONs. */ if (s->hblock_active && type != SCR_H2_F_CONTINUATION) { goto proto_err; @@ -1629,7 +1672,16 @@ proto_err: return false; } +static void scr_h2_on_data_in(void *ctx, const char *buf, size_t n); + +/* The socket's callbacks run in the session's context. */ static void scr_h2_on_data(void *ctx, const char *buf, size_t n) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_data_in(ctx, buf, n); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_data_in(void *ctx, const char *buf, size_t n) { ScrH2Session *s = ctx; if (s->destroyed) return; /* append to the reassembly buffer */ @@ -1680,7 +1732,15 @@ done: scr_http2_session_release(s); } +static void scr_h2_on_eof_in(void *ctx); + static void scr_h2_on_eof(void *ctx) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_eof_in(ctx); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_eof_in(void *ctx) { ScrH2Session *s = ctx; if (s->destroyed) return; /* The peer is done sending. After a graceful GOAWAY exchange this is @@ -1688,7 +1748,15 @@ static void scr_h2_on_eof(void *ctx) { scr_h2_session_abort_streams(s); } +static void scr_h2_on_closed_in(void *ctx); + static void scr_h2_on_closed(void *ctx) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_closed_in(ctx); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_closed_in(void *ctx) { ScrH2Session *s = ctx; s->destroyed = true; scr_h2_session_abort_streams(s); @@ -1700,7 +1768,16 @@ static void scr_h2_on_closed(void *ctx) { } } +static bool scr_h2_on_err_in(void *ctx, ScrStr *msg); + static bool scr_h2_on_err(void *ctx, ScrStr *msg) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + bool consumed = scr_h2_on_err_in(ctx, msg); + scr_als_dispatch_leave(prev); + return consumed; +} + +static bool scr_h2_on_err_in(void *ctx, ScrStr *msg) { ScrH2Session *s = ctx; if (s->err_ls.n == 0 && (s->srv == NULL || s->srv->session_error_ls.n == 0)) { return false; /* the socket's error story applies */ @@ -1716,6 +1793,7 @@ static ScrH2Session *scr_h2_session_new(bool client) { ScrH2Session *s = calloc(1, sizeof *s); if (!s) scr_h2_oom(); s->rc = 1; + s->als = scr_als_capture(); s->client = client; s->preface_done = client; /* clients never RECEIVE the magic */ s->next_stream_id = 1; @@ -2287,6 +2365,8 @@ static void scr_h2_upgrade_hook(void *ctx, ScrClosure *cb /*moves*/, void *fn, b * 'request' listeners (this = the net server, the http/1 shape). */ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2Headers *h, bool end_stream, ScrNetLs *listeners) { + /* The compat pair's own events follow the stream's context. */ + ScrAlsCtx *prev = scr_als_dispatch_enter(st->als); ScrHttpReq *req = scr_http_h2_req_new(s->sock, st); for (size_t i = 0; i < h->n; i++) { ScrStr *name = h->names[i]; @@ -2301,6 +2381,7 @@ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2 scr_http_h2_req_header(req, name, value); } ScrHttpRes *res = scr_http_h2_res_new(s->sock, st); + scr_als_dispatch_leave(prev); scr_http_res_set_req(res, req); /* res.req — the compat pair's backref */ st->compat_req = scr_http_req_retain(req); st->compat_res = scr_http_res_retain(res); @@ -2342,7 +2423,7 @@ static void scr_h2_srv_ctx_settle(void *p) { static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) { ScrH2SrvCtx *srv = (ScrH2SrvCtx *)ctx; - ScrH2Session *sess = scr_h2_session_new(false); + ScrH2Session *sess = scr_h2_session_new(false); /* in the listen() context */ sess->srv = scr_h2_srv_ctx_retain(srv); sess->sock = scr_net_sock_retain(sock); sess->next_stream_id = 2; /* server-initiated ids are even (push, unused) */ @@ -2354,6 +2435,14 @@ static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) { scr_h2_emit_push(SCR_H2_EMIT_SERVER_SESSION, scr_http2_session_retain(sess)); } +/* An accepted h2c connection: the socket's own events run with no store, + * as an accepted net socket's do (a TLS server's sockets keep the listen() + * context); the session keeps the listen() context the accept runs in. */ +static void scr_h2_on_plain_connection(void *ctx, ScrNetSocket *sock) { + scr_net_sock_set_context(sock, NULL); + scr_h2_on_connection(ctx, sock); +} + ScrNetServer *scr_http2_create_server(void) { scr_http2_install(); ScrNetServer *s = scr_net_create_server(NULL, NULL); @@ -2361,7 +2450,7 @@ ScrNetServer *scr_http2_create_server(void) { if (!ctx) scr_h2_oom(); ctx->proto = SCR_NET_PROTO_H2; ctx->rc = 1; - scr_net_server_set_native_conn(s, &scr_h2_on_connection, ctx, &scr_h2_srv_ctx_free_v); + scr_net_server_set_native_conn(s, &scr_h2_on_plain_connection, ctx, &scr_h2_srv_ctx_free_v); scr_net_server_set_http_ctx(s, ctx); scr_net_server_set_proto_settle(s, &scr_h2_srv_ctx_settle); return s; diff --git a/tests/corpus/7385-als-child-process-events.ts b/tests/corpus/7385-als-child-process-events.ts new file mode 100644 index 000000000..54fd80c3a --- /dev/null +++ b/tests/corpus/7385-als-child-process-events.ts @@ -0,0 +1,104 @@ +// AsyncLocalStorage contexts of node:child_process events. A child's +// 'spawn', 'error', 'exit' and 'close' events and its stdout and stderr +// 'data' and 'end' events run in the context of the spawn() call, +// whatever context registered the listener or killed the child. stdin's +// 'finish' runs in the context of the end() call, and its 'drain' and +// 'error' in the context of the write() call. Lines are sorted at the end: +// the stdout and stderr events of one child may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import { spawn } from "node:child_process"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; +const step = (fn: (done: () => void) => void): Promise => + new Promise((resolve) => fn(resolve)); + +await step((done) => { + const child = store.run("spawn", () => spawn("/bin/sh", ["-c", "printf out; printf err >&2"])); + store.run("listen", () => { + child.on("spawn", () => log("spawn event")); + child.stdout!.on("data", () => log("stdout data")); + child.stdout!.on("end", () => log("stdout end")); + child.stderr!.on("data", () => log("stderr data")); + child.on("exit", () => log("exit")); + child.on("close", () => { + log("close"); + done(); + }); + }); +}); + +await step((done) => { + const child = store.run("spawn-cat", () => spawn("/bin/cat")); + child.stdout!.on("data", () => log("cat data")); + store.run("stdin-write", () => { + child.stdin!.write("x"); + }); + child.stdin!.on("finish", () => log("stdin finish")); + store.run("stdin-end", () => { + child.stdin!.end(); + }); + child.on("close", () => { + log("cat close"); + done(); + }); +}); + +await step((done) => { + const child = store.run("spawn-missing", () => spawn("/nonexistent/command")); + child.on("error", () => log("spawn error")); + child.on("close", () => { + log("missing close"); + done(); + }); +}); + +await step((done) => { + const child = store.run("spawn-sleep", () => spawn("/bin/sleep", ["5"])); + child.on("exit", () => log("killed exit")); + child.on("close", () => { + log("killed close"); + done(); + }); + setTimeout(() => store.run("kill", () => child.kill()), 20); +}); + +// A write that fills the pipe: 'drain' runs in the write() call's context. +await step((done) => { + const child = store.run("spawn-slow", () => + spawn("/bin/sh", ["-c", "sleep 0.2; cat > /dev/null"]), + ); + child.stdin!.on("drain", () => { + log("stdin drain"); + child.stdin!.end(); + }); + child.on("close", () => { + log("slow close"); + done(); + }); + store.run("large-write", () => { + child.stdin!.write(Buffer.alloc(1 << 22, 97)); + }); +}); + +// A write to a child that closed its stdin: 'error' runs in the write() +// call's context. +await step((done) => { + const child = store.run("spawn-closed", () => spawn("/bin/sh", ["-c", "exec 0<&-; sleep 0.1"])); + child.stdin!.on("error", () => log("stdin error")); + child.on("close", () => { + log("closed close"); + done(); + }); + setTimeout(() => { + store.run("late-write", () => { + child.stdin!.write(Buffer.alloc(1 << 20, 97)); + }); + }, 50); +}); + +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7386-als-child-fork-events/main.mjs b/tests/corpus/7386-als-child-fork-events/main.mjs new file mode 100644 index 000000000..18b410643 --- /dev/null +++ b/tests/corpus/7386-als-child-fork-events/main.mjs @@ -0,0 +1,25 @@ +// AsyncLocalStorage contexts of a forked child's IPC events. The child's +// 'message', 'disconnect', 'exit' and 'close' events run in the context of +// the fork() call, whatever context registered the listener; a send() +// callback runs in the context of the send() call. +import { AsyncLocalStorage } from "node:async_hooks"; +import childProcess from "node:child_process"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +const child = store.run("fork", () => + childProcess.fork(new URL("./worker.mjs", import.meta.url), []), +); +store.run("listen", () => { + child.on("message", (message) => log(`message ${JSON.stringify(message)}`)); +}); +child.on("disconnect", () => log("disconnect")); +store.run("send", () => child.send("hi", () => log("send callback"))); +setTimeout(() => store.run("send-bye", () => child.send("bye")), 50); +child.on("exit", () => log("exit")); +child.on("close", () => { + log("close"); + for (const line of lines) console.log(line); +}); diff --git a/tests/corpus/7386-als-child-fork-events/worker.mjs b/tests/corpus/7386-als-child-fork-events/worker.mjs new file mode 100644 index 000000000..79f4bfdc6 --- /dev/null +++ b/tests/corpus/7386-als-child-fork-events/worker.mjs @@ -0,0 +1,4 @@ +process.on("message", (m) => { + process.send({ echo: m }); + if (m === "bye") process.disconnect(); +}); diff --git a/tests/corpus/7387-als-http2-events.mjs b/tests/corpus/7387-als-http2-events.mjs new file mode 100644 index 000000000..4dd17916e --- /dev/null +++ b/tests/corpus/7387-als-http2-events.mjs @@ -0,0 +1,91 @@ +// AsyncLocalStorage contexts of node:http2 events. A client session's +// 'connect' and 'close' run in the context of the http2.connect() call, +// and a client stream's 'response', 'data', 'end' and 'close' in the +// context of the request() call. A server's 'session' and 'stream' events +// run in the context of its listen() call; a server stream's own 'data', +// 'end' and 'close' events run with no store, as does a server.close() +// callback that waits for the sessions. Lines are sorted at the end: +// events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import http2 from "node:http2"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function requestScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => http2.createServer()); + server.on("session", () => log("server session")); + server.on("stream", (stream) => { + log("server stream"); + stream.on("data", () => log("server stream data")); + stream.on("end", () => { + log("server stream end"); + store.run("respond", () => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + }); + }); + store.run("listen", () => + server.listen(0, () => { + log("listening"); + const client = store.run("connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + client.on("connect", () => log("client connect")); + client.on("close", () => log("client session close")); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("end", () => { + log("client end"); + store.run("close", () => { + client.close(); + server.close(() => { + log("server close callback"); + resolve(); + }); + }); + }); + store.run("write", () => { + req.write("x"); + req.end(); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function resetScenario() { + return new Promise((resolve) => { + const server = store.run("reset-create", () => http2.createServer()); + server.on("stream", (stream) => { + log("reset server stream"); + stream.on("close", () => log("reset server stream close")); + store.run("reset", () => stream.close(http2.constants.NGHTTP2_CANCEL)); + }); + store.run("reset-listen", () => + server.listen(0, () => { + const client = store.run("reset-connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + const req = store.run("reset-request", () => client.request({ ":path": "/" })); + req.on("close", () => { + log("reset client close"); + client.close(); + server.close(() => resolve()); + }); + req.end(); + }), + ); + }); +} + +await requestScenario(); +await resetScenario(); +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7388-als-http2-compat-events.mjs b/tests/corpus/7388-als-http2-compat-events.mjs new file mode 100644 index 000000000..da44dc378 --- /dev/null +++ b/tests/corpus/7388-als-http2-compat-events.mjs @@ -0,0 +1,49 @@ +// AsyncLocalStorage contexts of the node:http2 compatibility API. The +// createServer() request handler runs in the context of the server's +// listen() call. The request's 'end' and the response's 'finish' and +// 'close' follow the server stream, which runs with no store, whatever +// context calls res.end(). A client stream's 'response', 'data' and +// 'close' run in the context of the request() call, and the client +// session's 'close' in the context of the http2.connect() call. Lines are +// sorted at the end: events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import http2 from "node:http2"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +const server = store.run("create", () => + http2.createServer((req, res) => { + log("request handler"); + req.resume(); + req.on("end", () => { + log("request end"); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + store.run("respond", () => res.end("done")); + }); + }), +); +store.run("listen", () => + server.listen(0, () => { + const client = store.run("connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + client.on("close", () => { + log("client session close"); + server.close(() => { + lines.sort(); + for (const line of lines) console.log(line); + }); + }); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/x" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("close", () => { + log("client stream close"); + store.run("close", () => client.close()); + }); + store.run("write", () => req.end("body")); + }), +); diff --git a/tests/corpus/7389-als-http2-secure-events.mjs b/tests/corpus/7389-als-http2-secure-events.mjs new file mode 100644 index 000000000..0772b5267 --- /dev/null +++ b/tests/corpus/7389-als-http2-secure-events.mjs @@ -0,0 +1,94 @@ +// AsyncLocalStorage contexts of node:http2 events over TLS +// (http2.createSecureServer and an https: authority). They follow corpus +// 7387's rules, with one difference that comes from the transport: a TLS +// server's connections run in the context of its listen() call, so a +// server.close() callback that waits for the sessions runs there too. +// Lines are sorted at the end: events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import fs from "node:fs"; +import http2 from "node:http2"; + +const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); +const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function requestScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => http2.createSecureServer({ key, cert })); + server.on("session", () => log("server session")); + server.on("stream", (stream) => { + log("server stream"); + stream.on("data", () => log("server stream data")); + stream.on("end", () => { + log("server stream end"); + store.run("respond", () => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + }); + }); + store.run("listen", () => + server.listen(0, () => { + log("listening"); + const client = store.run("connect", () => + http2.connect(`https://localhost:${server.address().port}`, { ca }), + ); + client.on("connect", () => log("client connect")); + client.on("close", () => log("client session close")); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("end", () => { + log("client end"); + store.run("close", () => { + client.close(); + server.close(() => { + log("server close callback"); + resolve(); + }); + }); + }); + store.run("write", () => { + req.write("x"); + req.end(); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function resetScenario() { + return new Promise((resolve) => { + const server = store.run("reset-create", () => http2.createSecureServer({ key, cert })); + server.on("stream", (stream) => { + log("reset server stream"); + stream.on("close", () => log("reset server stream close")); + store.run("reset", () => stream.close(http2.constants.NGHTTP2_CANCEL)); + }); + store.run("reset-listen", () => + server.listen(0, () => { + const client = store.run("reset-connect", () => + http2.connect(`https://localhost:${server.address().port}`, { ca }), + ); + const req = store.run("reset-request", () => client.request({ ":path": "/" })); + req.on("close", () => { + log("reset client close"); + client.close(); + server.close(() => resolve()); + }); + req.end(); + }), + ); + }); +} + +await requestScenario(); +await resetScenario(); +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7385-als-child-process-events.ts.json b/tests/harness/corpus-costs/7385-als-child-process-events.ts.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7385-als-child-process-events.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json b/tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7387-als-http2-events.mjs.json b/tests/harness/corpus-costs/7387-als-http2-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7387-als-http2-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json b/tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json b/tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } From 0e0458f29234d104d43c55a753a82703459545d0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:55:25 -0700 Subject: [PATCH 14/45] Keep literal key order through self copies and conversions that build no records A literal that starts by spreading a value of its own type, such as `{ ...current, ...patch, updatedAt }`, made the key-order witness give up on the type: the copy's source order was the type's own order, seeded with the declared table, so it conflicted with the literals that build the values. While no checked conversion builds the type, every value has the one chosen order and the copy adds no witness. Checked conversions also counted as builders of records they cannot create: a class instance's field records, a checked function's record parameters, a boxed primitive (the undefined a dynamic construction passes for a missing argument), and the result of a typed call of a method the program may replace on its instance, which without a replacement is the method's own native value boxed and checked back. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-callbacks.ts | 13 +- .../lowering/key-order-witness.test.ts | 118 +++++++++++++++++ .../frontend/lowering/key-order-witness.ts | 125 +++++++++++++++--- packages/compiler/src/ir/traverse.ts | 14 +- .../main.ts.json | 7 + .../corpus/7737-key-order-self-spread.ts.json | 6 + .../main.ts | 106 +++++++++++++++ .../registry.js | 21 +++ tests/corpus/7737-key-order-self-spread.ts | 34 +++++ .../main.ts.json | 1 + .../7737-key-order-self-spread.ts.json | 1 + 11 files changed, 421 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json create mode 100644 tests/corpus/7736-key-order-replaceable-method-result/main.ts create mode 100644 tests/corpus/7736-key-order-replaceable-method-result/registry.js create mode 100644 tests/corpus/7737-key-order-self-spread.ts create mode 100644 tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json create mode 100644 tests/harness/corpus-costs/7737-key-order-self-spread.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 0bda0068e..f92f42a80 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -156,6 +156,13 @@ export function classCallbackValue( }; } +/** Notes a checked conversion of a replaceable method's result for the + * key-order witness: the result is a value the method or its replacement + * returns, not a record built from a dynamic value. */ +function methodResult(lowerer: Lowerer, checked: IrExpr): IrExpr { + return checked.kind === "dynCheck" ? lowerer.keyOrder.methodResult(checked) : checked; +} + export function classCallbackCall( lowerer: Lowerer, receiver: IrExpr, @@ -197,7 +204,7 @@ export function classCallbackCall( // generator arm does not box) keeps its native type: the unchanged // method's result stays as it is, and a replacement's dynamic result // takes the checked conversion into that type. - const converted = lowerer.coerceToExpected(call, fallback.type); + const converted = methodResult(lowerer, lowerer.coerceToExpected(call, fallback.type)); if (!typeEquals(converted.type, fallback.type)) { lowerer.pushDiag( unsupportedDiag( @@ -234,7 +241,9 @@ export function classCallbackCall( loc, }; answer = - fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + fallback.type.kind === "void" + ? result + : methodResult(lowerer, lowerer.coerceToExpected(result, fallback.type)); } // Read after observedSlot built the prototype data and its originals. const originalId = observed ? original?.originalId() : undefined; diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index c85902c96..dc9149119 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -700,3 +700,121 @@ test("a spread reads its source's order right after the literal evaluates the so const snapshot = (prologue[2] as IrStmt & { kind: "varDecl" }).localId; expect(orderIndexOf(lit)).toMatchObject({ kind: "varRef", localId: snapshot }); }); + +/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }` + * plus `{ ...value, a }` of the same shape, beside `extra`. */ +function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[]): string[] { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false }; + f.run([ + decl("v", f.literal(ab, ["b", "a"])), + observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])), + ...extra(f, ab), + ]); + return f.order(ab)!; +} + +test("a literal that starts by spreading its own shape takes the other literals' order", () => { + expect(selfCopy(() => [])).toEqual(["b", "a"]); +}); + +test("a self copy of a shape a checked conversion builds cannot be ordered statically", () => { + const parsed = (ab: IrType): IrExpr => ({ + kind: "dynCheck", + value: ref("j", { kind: "dyn" }), + type: ab, + loc, + }); + expect(selfCopy((_, ab) => [observe(parsed(ab))])).toEqual(["a", "b"]); +}); + +test("a replaceable method's checked result builds no record", () => { + expect( + selfCopy((f, ab) => { + const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc }; + return [observe(f.notes.methodResult(result))]; + }), + ).toEqual(["b", "a"]); +}); + +test("a replaceable method's result keeps its witness through an IR rewrite", () => { + expect( + selfCopy((f, ab) => { + const result = f.notes.methodResult({ + kind: "dynCheck", + value: ref("r", { kind: "dyn" }), + type: ab, + loc, + }); + const identity = { expr: (e: IrExpr): IrExpr => e, stmt: (st: IrStmt): IrStmt => st }; + observeRecordLiteralRebuilds((from, to) => f.notes.follow(from, to)); + try { + const rebuilt = transformExpr(result, identity); + expect(rebuilt).not.toBe(result); + return [observe(rebuilt)]; + } finally { + observeRecordLiteralRebuilds(null); + } + }), + ).toEqual(["b", "a"]); +}); + +test("a checked conversion of a boxed primitive builds no record", () => { + expect( + selfCopy((_, ab) => { + const missing: IrExpr = { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: { kind: "undefinedT" }, loc }, + type: { kind: "dyn" }, + loc, + }; + return [observe({ kind: "dynCheck", value: missing, type: ab, loc })]; + }), + ).toEqual(["b", "a"]); +}); + +test("a checked conversion into a class builds none of its fields' records", () => { + expect( + selfCopy((f, ab) => { + (f.lowerer.classes as Map).set("Holder", { + def: { fields: [{ name: "value", type: ab }] }, + }); + const holder: IrType = { kind: "object", className: "Holder" }; + const checked: IrExpr = { + kind: "dynCheck", + value: ref("h", { kind: "dyn" }), + type: holder, + loc, + }; + return [{ kind: "exprStmt", expr: checked, loc }]; + }), + ).toEqual(["b", "a"]); +}); + +test("a checked function passes its record parameters out, and its callbacks' back in", () => { + const callable = (params: IrType[]): IrType => ({ + kind: "func", + params, + ret: { kind: "string" }, + }); + const checkedAs = (type: IrType): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "dynCheck", value: ref("fn", { kind: "dyn" }), type, loc }, + loc, + }); + expect(selfCopy((_, ab) => [checkedAs(callable([ab]))])).toEqual(["b", "a"]); + // A callback parameter receives values the JavaScript function builds. + expect(selfCopy((_, ab) => [checkedAs(callable([callable([ab])]))])).toEqual(["a", "b"]); + // So does a container parameter, which the function can write into. + expect(selfCopy((_, ab) => [checkedAs(callable([{ kind: "array", elem: ab }]))])).toEqual([ + "a", + "b", + ]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index aa84c68c2..2f573e676 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -28,7 +28,19 @@ * build keeps today's table, since another table would make values * parsed in today's order keep a source view; a shape object literals * also build takes the literals' order. Their order is unknown, so a - * copy out of such a shape cannot be ordered statically; + * copy out of such a shape cannot be ordered statically. A conversion + * builds only the records it can create from a JavaScript value: none + * for a boxed primitive or a class instance (a native instance whose + * fields its own writes fill), none for a checked function's record + * parameters, and none for the checked result of a typed call of a + * method the program may replace on its instance. Without a replacement + * that result is the method's own native value, boxed and checked back + * unchanged; a replacement's result keeps its own order in its record + * source, and only copies out of it follow the chosen table, as they + * followed today's; + * - a literal that starts by spreading a value of its own shape copies + * that value's order. While no checked conversion builds the shape, + * every value has the one chosen order, so the copy adds no witness; * - a write to an optional key that may be absent appends it in Node. A * key no construction creates ("late-only") goes after every key a * construction can create; any other moving write goes after every key. @@ -112,6 +124,7 @@ export type KeyOrderSegment = export class KeyOrderNotes { private readonly literals = new Map(); private readonly copies = new Map(); + private readonly methodResults = new Set(); /** Shapes a frontend order surface (keys, values/entries, inspect, * index-signature merges) walks. */ readonly observed = new Set(); @@ -128,13 +141,21 @@ export class KeyOrderNotes { return node; } + /** The checked result of a typed call of a method the program may + * replace on its instance (class-callbacks.ts classCallbackCall). */ + methodResult(node: IrExpr): IrExpr { + this.methodResults.add(node); + return node; + } + /** A pass rebuilt `from` as `to` (traverse.ts mapExprChildren): the - * rebuilt literal is the same construction, so it keeps the witness. */ + * rebuilt node is the same construction, so it keeps the witness. */ follow(from: IrExpr, to: IrExpr): void { const segments = this.literals.get(from); if (segments !== undefined) this.literals.set(to, segments); const source = this.copies.get(from); if (source !== undefined) this.copies.set(to, source); + if (this.methodResults.has(from)) this.methodResults.add(to); } observe(type: IrType): void { @@ -148,6 +169,10 @@ export class KeyOrderNotes { copyOf(node: IrExpr): string | undefined { return this.copies.get(node); } + + isMethodResult(node: IrExpr): boolean { + return this.methodResults.has(node); + } } const isArrayIndex = (name: string): boolean => { @@ -201,6 +226,23 @@ function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { ); } +const PRIMITIVE_KINDS = new Set([ + "f64", + "bigint", + "string", + "bool", + "undefinedT", + "nullT", + "symbol", +]); + +/** A checked conversion of a boxed primitive (the `undefined` a dynamic + * construction passes for a missing argument) answers that primitive or + * throws: it builds no record. */ +function primitiveSource(node: IrExpr & { kind: "dynCheck" }): boolean { + return node.value.kind === "dynFrom" && PRIMITIVE_KINDS.has(node.value.value.type.kind); +} + /** Chooses each in-scope record shape's enumeration table from its * witnesses. Runs once retained reachability has settled the historical * tables and derived metadata, before the order-snapshotting helper bodies @@ -239,35 +281,53 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio /** Every record shape a type can hold, through fields, arms, elements, * callables, promises and class fields. */ /** Each walk shares one `seen` set per result set, so every type graph - * is walked once however many roots reach it. */ - const shapesIn = (type: IrType, out: Set, seen: Set): void => { - const visit = (t: IrType): boolean => { + * is walked once however many roots reach it. + * + * `produced` walks only what a checked conversion into the type can + * build. It answers an existing native instance for a class type (a plain + * object cannot acquire a class brand; only %Error rebuilds), so the + * records the instance's fields hold come from the class's own writes and + * its checked field setters, each a witness of its own. A checked + * function's parameters receive native values: a record parameter is + * passed out, never built, though a callback among them receives values + * built from JavaScript again. */ + const shapesIn = (type: IrType, out: Set, seen: Set, produced = false): void => { + const visit = (t: IrType, built: boolean): boolean => { + const mark = built ? "" : "-"; if (t.kind === "record") { - if (seen.has("r:" + t.shapeId)) return true; - seen.add("r:" + t.shapeId); - out.add(t.shapeId); + if (seen.has(mark + "r:" + t.shapeId)) return true; + seen.add(mark + "r:" + t.shapeId); + if (built) out.add(t.shapeId); const shape = shapes.get(t.shapeId); if (shape) { - for (const f of shape.fields) visit(f.type); - if (shape.indexValue) visit(shape.indexValue); + // A record passed out can be written through: its fields take + // values JavaScript builds. + for (const f of shape.fields) visit(f.type, true); + if (shape.indexValue) visit(shape.indexValue, true); } return true; } if (t.kind === "union") { - if (seen.has("u:" + t.unionId)) return true; - seen.add("u:" + t.unionId); - for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm); + if (seen.has(mark + "u:" + t.unionId)) return true; + seen.add(mark + "u:" + t.unionId); + for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm, built); return true; } if (t.kind === "object") { + if (produced && t.className !== "%Error") return true; if (seen.has("c:" + t.className)) return true; seen.add("c:" + t.className); - for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type, true); return true; } - return everyTypeChild(t, visit); + if (produced && t.kind === "func") { + for (const param of t.params) visit(param, !built); + return visit(t.ret, built); + } + // Containers passed out stay writable: their elements count as built. + return everyTypeChild(t, (child) => visit(child, true)); }; - visit(type); + visit(type, true); }; const opaqueShapes = new Set(); const opaqueSeen = new Set(); @@ -280,7 +340,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const declaredSeen = new Set(); const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); - else shapesIn(type, runtimeShapes, runtimeSeen); + else shapesIn(type, runtimeShapes, runtimeSeen, true); }; // ── collection ────────────────────────────────────────────────────── @@ -388,7 +448,12 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio return; case "dynCheck": if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); - else if (!orderPreservingClone(e)) producedBy(e.type, false); + // Conversions that build no record: a boxed primitive, and a + // replaceable method's result, which is the value the method + // returns (boxed and checked back unchanged) or what a replacement + // installed on the instance returns. + else if (!orderPreservingClone(e) && !primitiveSource(e) && !notes.isMethodResult(e)) + producedBy(e.type, false); return; case "dynDestrCheck": case "caughtCheck": @@ -649,6 +714,29 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio for (const c of f.copies) deps.add(c.source); return [...deps]; }; + /** `{ ...value, ...rest }` that starts with a spread of a value of the + * literal's own shape holding every key: it creates its keys in the + * value's order, and the later segments only overwrite them. With no + * checked conversion building the shape, every value enumerates in the + * one table this pass chooses, so the copy agrees with any choice and + * adds no edge. */ + const copiesItself = ( + id: string, + f: ShapeFacts, + table: readonly string[], + segments: readonly KeyOrderSegment[], + ): boolean => { + const first = segments[0]; + return ( + first !== undefined && + first.kind === "spread" && + first.shapeId === id && + !first.maybeAbsent && + !f.runtime && + segments.every((s) => s.kind !== "opaque") && + table.every((k) => !maybeAbsent(id, k)) + ); + }; /** One shape's order given its sources' current orders. */ const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { const shape = shapes.get(id); @@ -666,6 +754,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const edges: [string, string][] = []; if (f.pinned) chain(table, edges); for (const segments of f.literals) { + if (copiesItself(id, f, table, segments)) continue; const sequences = literalSequences(segments, keySet, current); if (sequences === null) return "unknown"; for (const seq of sequences) chain(seq, edges); diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 251904351..80425a056 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -473,9 +473,10 @@ export function everyStmtChild( throw new Error("unhandled IR node"); } -/** Called with each record literal mapExprChildren rebuilds and its - * replacement, so a side table keyed by node identity (the lowering's - * key-order witness notes) follows the literal into the rewritten tree. */ +/** Called with each record literal and checked conversion mapExprChildren + * rebuilds and its replacement, so a side table keyed by node identity (the + * lowering's key-order witness notes) follows the node into the rewritten + * tree. */ let recordLiteralRebuilt: ((from: IrExpr, to: IrExpr) => void) | null = null; /** Installs (or with null removes) the record-literal rebuild observer. */ @@ -504,8 +505,11 @@ export function mapExprChildren( return node; case "libCall": return { ...node, args: mapOf(node.args, expr) }; - case "dynCheck": - return { ...node, value: expr(node.value) }; + case "dynCheck": { + const rebuilt: IrExpr = { ...node, value: expr(node.value) }; + if (recordLiteralRebuilt !== null) recordLiteralRebuilt(node, rebuilt); + return rebuilt; + } case "strEq": return { ...node, left: expr(node.left), right: expr(node.right) }; case "fieldGet": diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json new file mode 100644 index 000000000..006d4a8c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7736-key-order-replaceable-method-result/registry.js", + "/tests/corpus/7736-key-order-replaceable-method-result/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json new file mode 100644 index 000000000..97f2ca7bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7737-key-order-self-spread.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7736-key-order-replaceable-method-result/main.ts b/tests/corpus/7736-key-order-replaceable-method-result/main.ts new file mode 100644 index 000000000..5ebbfa761 --- /dev/null +++ b/tests/corpus/7736-key-order-replaceable-method-result/main.ts @@ -0,0 +1,106 @@ +import { Registry, describe, titleIndex } from "./registry.js"; + +// Records keep the key order their object literals write when values of +// the same type also pass through checked conversions that build none of +// them: +// - a typed method the program may replace on its instance (the +// computed-key write in registry.js makes every method name replaceable) +// returns the records it stores, checked back from the guarded call; +// - an index spec from JavaScript, checked into a type whose function +// field takes a Book, receives Books but never builds one. +// JSON, Object.keys and spreads of those records follow insertion order, +// not the interface's declared order. +interface Book { + id: string; + title: string; + author: string; + createdAt: string; + updatedAt: string; +} + +type Draft = { title: string; author: string }; + +interface IndexSpec { + name: string; + key: (row: T) => string; +} + +class Repository { + readonly #rows = new Map(); + + insert(row: T): T { + this.#rows.set(row.id, row); + return row; + } + + require(id: string): T { + const row = this.#rows.get(id); + if (row === undefined) throw new Error(`missing ${id}`); + return row; + } + + find(id: string): T | undefined { + return this.#rows.get(id); + } + + all(): T[] { + return [...this.#rows.values()]; + } +} + +class Service { + readonly #books = new Repository(); + readonly #index: IndexSpec = titleIndex; + #clock = 0; + + #now(): string { + this.#clock++; + return `t${this.#clock}`; + } + + create(draft: Draft, id: string): Book { + const at = this.#now(); + return this.#books.insert({ ...draft, id, createdAt: at, updatedAt: at }); + } + + update(id: string, patch: Partial): Book { + const current = this.#books.require(id); + return this.#books.insert({ ...current, ...patch, updatedAt: this.#now() }); + } + + get(id: string): Book { + return this.#books.require(id); + } + + maybe(id: string): Book | undefined { + return this.#books.find(id); + } + + list(): Book[] { + return this.#books.all(); + } + + indexed(): string[] { + return this.list().map((book) => `${this.#index.name}=${this.#index.key(book)}`); + } +} + +const registry = new Registry(); +console.log(describe(registry)); + +const service = new Service(); +console.log(JSON.stringify(service.create({ title: "Dune", author: "Herbert" }, "b1"))); +console.log(JSON.stringify(service.create({ title: "Earthsea", author: "Le Guin" }, "b2"))); +console.log(JSON.stringify(service.update("b1", { title: "Dune Messiah" }))); +console.log(Object.keys(service.get("b2")).join(",")); +const found = service.maybe("b1"); +if (found !== undefined) console.log(JSON.stringify(found)); +console.log(JSON.stringify(service.list())); +const copy = { ...service.get("b1") }; +console.log(Object.keys(copy).join(",")); +console.log(service.indexed().join(" ")); +try { + service.get("missing"); +} catch (error) { + console.log((error as Error).message); +} diff --git a/tests/corpus/7736-key-order-replaceable-method-result/registry.js b/tests/corpus/7736-key-order-replaceable-method-result/registry.js new file mode 100644 index 000000000..d6da1edc5 --- /dev/null +++ b/tests/corpus/7736-key-order-replaceable-method-result/registry.js @@ -0,0 +1,21 @@ +// Installs one handler per method name with a computed-key write, the way +// router classes attach `get`, `post`, ... in their constructor. The +// compiler cannot tell which names such a write replaces. +const METHODS = ["get", "post"]; + +export class Registry { + constructor() { + METHODS.forEach((method) => { + this[method] = (...args) => `${method}:${args.length}`; + }); + } +} + +export function describe(registry) { + return `${registry.get(1, 2)} ${registry.post()}`; +} + +export const titleIndex = { + name: "title", + key: (row) => row.title.toLowerCase(), +}; diff --git a/tests/corpus/7737-key-order-self-spread.ts b/tests/corpus/7737-key-order-self-spread.ts new file mode 100644 index 000000000..5577f7531 --- /dev/null +++ b/tests/corpus/7737-key-order-self-spread.ts @@ -0,0 +1,34 @@ +// A literal that starts by spreading a value of its own type copies that +// value's keys in their order, then overwrites some of them. It must not +// stop the type from taking the order its other literals write: the +// records below enumerate as created (title before id), not in the +// interface's declared order. +interface Book { + id: string; + title: string; + tags: string[]; + updatedAt: string; + version: number; +} + +type Draft = { title: string; tags: string[] }; + +function create(draft: Draft, id: string): Book { + return { ...draft, id, updatedAt: "t1", version: 1 }; +} + +function update(current: Book, patch: Partial): Book { + return { ...current, ...patch, updatedAt: "t2", version: current.version + 1 }; +} + +function touch(current: Book): Book { + return { ...current, version: current.version + 1 }; +} + +const book = create({ title: "Dune", tags: ["sf"] }, "b1"); +console.log(JSON.stringify(book)); +const updated = update(book, { title: "Dune Messiah" }); +console.log(JSON.stringify(updated)); +console.log(Object.keys(touch(updated)).join(",")); +console.log(Object.keys(update(updated, { tags: [] })).join(" ")); +for (const key in touch(book)) console.log(key); diff --git a/tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json b/tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json new file mode 100644 index 000000000..6a79d2c04 --- /dev/null +++ b/tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json @@ -0,0 +1 @@ +{ "plain": 10900, "san": 17500 } diff --git a/tests/harness/corpus-costs/7737-key-order-self-spread.ts.json b/tests/harness/corpus-costs/7737-key-order-self-spread.ts.json new file mode 100644 index 000000000..de3f40849 --- /dev/null +++ b/tests/harness/corpus-costs/7737-key-order-self-spread.ts.json @@ -0,0 +1 @@ +{ "plain": 3800, "san": 6100 } From 6f7d1960677efbe0835b85b20c766cda166d8f7a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 06:47:08 -0500 Subject: [PATCH 15/45] Count records built for callbacks and replacements as built at run time The key-order witness treats records JavaScript builds as having a run-time order. Two cases this change narrowed are counted again: - A native callback that a checked JavaScript function finds inside a record or array it receives is called with records JavaScript builds. The walk of a checked function's parameters reset to the built direction at the fields and elements of the records it passes out, so the callback's own parameters counted as passed out. Children of a container passed out now count in both directions (written by JavaScript, and read out of it), and class instances keep the direction too. - A replaceable method's checked result builds a record when a replacement returns one. It is left out only while every site the census found for the method name reads it or assigns a function whose return type is primitive, as the handlers a router class installs through a computed key do. The census now records every such site (Lowerer.methodAccessSites). --- .../src/frontend/lowering/class-callbacks.ts | 8 +- .../lowering/class-method-mutations.ts | 18 +-- .../lowering/key-order-witness.test.ts | 34 ++++-- .../frontend/lowering/key-order-witness.ts | 108 ++++++++++++++---- .../src/frontend/lowering/lower-classes.ts | 4 +- .../compiler/src/frontend/lowering/lowerer.ts | 17 +++ .../main.ts.json | 7 ++ .../lib.js | 11 ++ .../main.ts | 50 ++++++++ .../main.ts.json | 1 + 10 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json create mode 100644 tests/corpus/7623-key-order-javascript-built-records/lib.js create mode 100644 tests/corpus/7623-key-order-javascript-built-records/main.ts create mode 100644 tests/harness/corpus-costs/7623-key-order-javascript-built-records/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index f92f42a80..3a1c7975d 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -159,8 +159,8 @@ export function classCallbackValue( /** Notes a checked conversion of a replaceable method's result for the * key-order witness: the result is a value the method or its replacement * returns, not a record built from a dynamic value. */ -function methodResult(lowerer: Lowerer, checked: IrExpr): IrExpr { - return checked.kind === "dynCheck" ? lowerer.keyOrder.methodResult(checked) : checked; +function methodResult(lowerer: Lowerer, checked: IrExpr, name: string): IrExpr { + return checked.kind === "dynCheck" ? lowerer.keyOrder.methodResult(checked, name) : checked; } export function classCallbackCall( @@ -204,7 +204,7 @@ export function classCallbackCall( // generator arm does not box) keeps its native type: the unchanged // method's result stays as it is, and a replacement's dynamic result // takes the checked conversion into that type. - const converted = methodResult(lowerer, lowerer.coerceToExpected(call, fallback.type)); + const converted = methodResult(lowerer, lowerer.coerceToExpected(call, fallback.type), name); if (!typeEquals(converted.type, fallback.type)) { lowerer.pushDiag( unsupportedDiag( @@ -243,7 +243,7 @@ export function classCallbackCall( answer = fallback.type.kind === "void" ? result - : methodResult(lowerer, lowerer.coerceToExpected(result, fallback.type)); + : methodResult(lowerer, lowerer.coerceToExpected(result, fallback.type), name); } // Read after observedSlot built the prototype data and its originals. const originalId = observed ? original?.originalId() : undefined; diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index fd23b6db8..035c2c2a5 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -84,7 +84,7 @@ export function collectClassMethodMutations( for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name)); const markName = (name: string, site: ts.Node, arrow = false): void => { if (name === "prototype" || name === "__proto__") unknownKey = site; - if (methods.has(name)) lowerer.prototypeMethodAccesses.set(name, site); + if (methods.has(name)) lowerer.noteMethodAccess(name, site); if (!arrow) receiverFields.add(name); }; const unwrap = (value: ts.Expression): ts.Expression => { @@ -395,10 +395,9 @@ export function collectClassMethodMutations( ts.isStringLiteralLike(base.argumentExpression) && base.argumentExpression.text === "prototype"); if (prototype) { - if (ts.isPropertyAccessExpression(node)) - lowerer.prototypeMethodAccesses.set(node.name.text, node); + if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node); else if (ts.isStringLiteralLike(node.argumentExpression)) - lowerer.prototypeMethodAccesses.set(node.argumentExpression.text, node); + lowerer.noteMethodAccess(node.argumentExpression.text, node); else markKey(node.argumentExpression, node); } } @@ -435,7 +434,7 @@ export function collectClassMethodMutations( ? lowerer.checker.getSymbolAtLocation(access.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name); if (symbol && symbol.declarations.some(ts.isMethodDeclaration)) - lowerer.prototypeMethodAccesses.set(name, access); + lowerer.noteMethodAccess(name, access); } lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]); for (const node of globals) @@ -571,9 +570,10 @@ export function collectClassMethodMutations( ); for (const { target, site } of propertyWrites) { const name = target.name.text; - if (lowerer.prototypeMethodAccesses.has(name) || functionFree(lowerer.typeOf(target))) continue; - if (!methodOwnerCanInhabit(target.expression, name)) continue; - lowerer.prototypeMethodAccesses.set(name, site); + if (functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name)) + continue; + if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site); + else lowerer.noteMethodAccess(name, site); } for (const { key, receiver, site } of computedWrites) { // A class method declared with a well-known symbol key is not a write; @@ -620,7 +620,7 @@ export function collectClassMethodMutations( } } if (unknownKey) { - for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey); + for (const method of methods) lowerer.noteMethodAccess(method, unknownKey); for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method); } if (!unknownKey && !unknownOwnField) diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index dc9149119..8058d0066 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -15,6 +15,7 @@ import { type KeyOrderSegment, } from "./key-order-witness.js"; import type { Lowerer } from "./lowerer.js"; +import * as ts from "../ts7/adapter.js"; const loc = { file: "order.ts", start: 0, end: 1 }; /** `number | undefined`: the optional-field slot type. */ @@ -40,6 +41,7 @@ function fixture(): Fixture { keyOrder: notes, unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]), classes: new Map(), + methodAccessSites: new Map(), armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ @@ -739,20 +741,38 @@ test("a replaceable method's checked result builds no record", () => { expect( selfCopy((f, ab) => { const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc }; - return [observe(f.notes.methodResult(result))]; + return [observe(f.notes.methodResult(result, "load"))]; }), ).toEqual(["b", "a"]); }); +test("a replaceable method's checked result builds records when a replacement may return one", () => { + expect( + selfCopy((f, ab) => { + // A site the census cannot see through (a computed write of some + // value, a reflection call) may install a replacement returning a + // record built in any order. + (f.lowerer.methodAccessSites as Map).set("load", [ + { kind: ts.SyntaxKind.CallExpression } as unknown as ts.Node, + ]); + const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc }; + return [observe(f.notes.methodResult(result, "load"))]; + }), + ).toEqual(["a", "b"]); +}); + test("a replaceable method's result keeps its witness through an IR rewrite", () => { expect( selfCopy((f, ab) => { - const result = f.notes.methodResult({ - kind: "dynCheck", - value: ref("r", { kind: "dyn" }), - type: ab, - loc, - }); + const result = f.notes.methodResult( + { + kind: "dynCheck", + value: ref("r", { kind: "dyn" }), + type: ab, + loc, + }, + "load", + ); const identity = { expr: (e: IrExpr): IrExpr => e, stmt: (st: IrStmt): IrStmt => st }; observeRecordLiteralRebuilds((from, to) => f.notes.follow(from, to)); try { diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 2f573e676..933c9efcd 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -69,6 +69,7 @@ import { transformStmtList, } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; +import * as ts from "../ts7/adapter.js"; import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; /** The statements a key-order helper runs for record `ref`: `walks[0]`, @@ -124,7 +125,7 @@ export type KeyOrderSegment = export class KeyOrderNotes { private readonly literals = new Map(); private readonly copies = new Map(); - private readonly methodResults = new Set(); + private readonly methodResults = new Map(); /** Shapes a frontend order surface (keys, values/entries, inspect, * index-signature merges) walks. */ readonly observed = new Set(); @@ -141,10 +142,10 @@ export class KeyOrderNotes { return node; } - /** The checked result of a typed call of a method the program may - * replace on its instance (class-callbacks.ts classCallbackCall). */ - methodResult(node: IrExpr): IrExpr { - this.methodResults.add(node); + /** The checked result of a typed call of method `name`, which the program + * may replace on its instance (class-callbacks.ts classCallbackCall). */ + methodResult(node: IrExpr, name: string): IrExpr { + this.methodResults.set(node, name); return node; } @@ -155,7 +156,8 @@ export class KeyOrderNotes { if (segments !== undefined) this.literals.set(to, segments); const source = this.copies.get(from); if (source !== undefined) this.copies.set(to, source); - if (this.methodResults.has(from)) this.methodResults.add(to); + const method = this.methodResults.get(from); + if (method !== undefined) this.methodResults.set(to, method); } observe(type: IrType): void { @@ -170,8 +172,9 @@ export class KeyOrderNotes { return this.copies.get(node); } - isMethodResult(node: IrExpr): boolean { - return this.methodResults.has(node); + /** The method whose checked result `node` is, if it is one. */ + methodResultOf(node: IrExpr): string | undefined { + return this.methodResults.get(node); } } @@ -243,6 +246,54 @@ function primitiveSource(node: IrExpr & { kind: "dynCheck" }): boolean { return node.value.kind === "dynFrom" && PRIMITIVE_KINDS.has(node.value.value.type.kind); } +/** Whether a census site for a method name can only read the method or + * install a replacement whose result holds no object: a read + * (`C.prototype.m`, `obj.m`) outside an assignment target, or an assignment + * of a function expression whose return type is primitive. Other sites (a + * computed write of another value, a reflection call, a property bag) may + * install a replacement that returns a record. */ +function replacesWithoutRecords(lowerer: Lowerer, site: ts.Node): boolean { + const parent = site.parent; + if (ts.isPropertyAccessExpression(site) || ts.isElementAccessExpression(site)) + return !( + parent !== undefined && + ts.isBinaryExpression(parent) && + parent.left === site && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ); + if (!ts.isBinaryExpression(site) || site.operatorToken.kind !== ts.SyntaxKind.EqualsToken) + return false; + let value = site.right; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (!ts.isArrowFunction(value) && !ts.isFunctionExpression(value)) return false; + const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(value)); + return ( + signatures.length > 0 && + signatures.every((signature) => + primitiveOnly(lowerer.checker.getReturnTypeOfSignature(signature)), + ) + ); +} + +/** A type whose values are primitives (or void): it holds no record. */ +function primitiveOnly(type: ts.Type): boolean { + if (type.isUnionType()) return ts.constituentTypes(type).every(primitiveOnly); + return ( + (type.flags & + (ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.ESSymbolLike | + ts.TypeFlags.Null | + ts.TypeFlags.Undefined | + ts.TypeFlags.Void | + ts.TypeFlags.Never)) !== + 0 + ); +} + /** Chooses each in-scope record shape's enumeration table from its * witnesses. Runs once retained reachability has settled the historical * tables and derived metadata, before the order-snapshotting helper bodies @@ -294,16 +345,24 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const shapesIn = (type: IrType, out: Set, seen: Set, produced = false): void => { const visit = (t: IrType, built: boolean): boolean => { const mark = built ? "" : "-"; + // A container JavaScript builds converts into fresh native storage, so + // its children are built too. One passed out stays shared: JavaScript + // writes into it (built children) and reads native values out of it, + // such as callbacks whose parameters it then builds (children passed + // out). Those children count in both directions. + const child = (type: IrType): boolean => { + visit(type, true); + if (!built) visit(type, false); + return true; + }; if (t.kind === "record") { if (seen.has(mark + "r:" + t.shapeId)) return true; seen.add(mark + "r:" + t.shapeId); if (built) out.add(t.shapeId); const shape = shapes.get(t.shapeId); if (shape) { - // A record passed out can be written through: its fields take - // values JavaScript builds. - for (const f of shape.fields) visit(f.type, true); - if (shape.indexValue) visit(shape.indexValue, true); + for (const f of shape.fields) child(f.type); + if (shape.indexValue) child(shape.indexValue); } return true; } @@ -315,17 +374,16 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio } if (t.kind === "object") { if (produced && t.className !== "%Error") return true; - if (seen.has("c:" + t.className)) return true; - seen.add("c:" + t.className); - for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type, true); + if (seen.has(mark + "c:" + t.className)) return true; + seen.add(mark + "c:" + t.className); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) child(f.type); return true; } if (produced && t.kind === "func") { for (const param of t.params) visit(param, !built); return visit(t.ret, built); } - // Containers passed out stay writable: their elements count as built. - return everyTypeChild(t, (child) => visit(child, true)); + return everyTypeChild(t, child); }; visit(type, true); }; @@ -338,6 +396,14 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio * declaration's order, the order their table holds. */ const declaredShapes = new Set(); const declaredSeen = new Set(); + /** The checked result of a replaceable method call while every site that + * can replace the method installs functions that return no record. */ + const unchangedMethodResult = (node: IrExpr): boolean => { + const name = notes.methodResultOf(node); + if (name === undefined) return false; + const sites = lowerer.methodAccessSites.get(name) ?? []; + return sites.every((site) => replacesWithoutRecords(lowerer, site)); + }; const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); else shapesIn(type, runtimeShapes, runtimeSeen, true); @@ -449,10 +515,10 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio case "dynCheck": if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); // Conversions that build no record: a boxed primitive, and a - // replaceable method's result, which is the value the method - // returns (boxed and checked back unchanged) or what a replacement - // installed on the instance returns. - else if (!orderPreservingClone(e) && !primitiveSource(e) && !notes.isMethodResult(e)) + // replaceable method's result while no replacement the program can + // install returns a record: then it is the method's own value, + // boxed and checked back unchanged. + else if (!orderPreservingClone(e) && !primitiveSource(e) && !unchangedMethodResult(e)) producedBy(e.type, false); return; case "dynDestrCheck": diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 29afbb4fe..56c70c5bd 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -4147,7 +4147,7 @@ export function collectClassShapeInner( // An own JS callable shadows its prototype declaration. // Keep the method in the prototype and store the assigned // value in the instance property bag. - lowerer.prototypeMethodAccesses.set(name, assign); + lowerer.noteMethodAccess(name, assign); continue; } const sym = lowerer.checker.getSymbolAtLocation(assign); @@ -4229,7 +4229,7 @@ export function collectClassShapeInner( } if (!key && (fields.has(fieldName) || hasAccessor(fieldName))) continue; if (!key && (methods.has(fieldName) || lowerer.findMethodOn(base, fieldName))) { - lowerer.prototypeMethodAccesses.set(fieldName, lhs); + lowerer.noteMethodAccess(fieldName, lhs); continue; } // A key already declared (own or inherited) makes later diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e36ffd09f..0922f8928 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3021,6 +3021,23 @@ export class Lowerer { /** Named method slots observed before class collection need a callable * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); + /** Every site the census found for a name in prototypeMethodAccesses (which + * keeps one): reads of the method and writes that may replace it. The + * key-order witness asks which replacements can return records. */ + readonly methodAccessSites = new Map(); + + /** Marks `name` as a method the program may read or replace at `site`. */ + noteMethodAccess(name: string, site: ts.Node): void { + this.prototypeMethodAccesses.set(name, site); + this.noteMethodSite(name, site); + } + + /** Records a site for `name` without changing its prototypeMethodAccesses entry. */ + noteMethodSite(name: string, site: ts.Node): void { + const sites = this.methodAccessSites.get(name); + if (sites === undefined) this.methodAccessSites.set(name, [site]); + else sites.push(site); + } /** Whether the program reads `Error.prototype`, the checked-dynamic * object an ES5 constructor function can inherit from, or has a class * whose heritage is evaluated at run time (`extends (Base ?? Error)`): diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json new file mode 100644 index 000000000..c3c2aa5b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7623-key-order-javascript-built-records/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7623-key-order-javascript-built-records/lib.js", + "/tests/corpus/7623-key-order-javascript-built-records/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7623-key-order-javascript-built-records/lib.js b/tests/corpus/7623-key-order-javascript-built-records/lib.js new file mode 100644 index 000000000..4ff312846 --- /dev/null +++ b/tests/corpus/7623-key-order-javascript-built-records/lib.js @@ -0,0 +1,11 @@ +// Records built in JavaScript in another key order than the TypeScript +// literals of the same type: a replacement method installed through a +// computed key, and a function stored on the global object, read through a +// typed view, that calls a callback inside its options record. +export function install(target, name) { + target[name] = () => ({ a: 1, b: 2 }); +} + +globalThis.__eachRow = function (options) { + options.visit({ x: 5, y: 6 }); +}; diff --git a/tests/corpus/7623-key-order-javascript-built-records/main.ts b/tests/corpus/7623-key-order-javascript-built-records/main.ts new file mode 100644 index 000000000..ae4876131 --- /dev/null +++ b/tests/corpus/7623-key-order-javascript-built-records/main.ts @@ -0,0 +1,50 @@ +// Copies and enumerations of records JavaScript builds follow the order +// JavaScript wrote, also when the TypeScript literals of the type write +// another order and spread their own type. Each case uses its own type. +import { install } from "./lib.js"; + +interface Pair { + a: number; + b: number; +} + +interface Row { + x: number; + y: number; +} + +interface Options { + visit: (row: Row) => void; +} + +class Store { + get(): Pair { + return { b: 2, a: 1 }; + } +} + +function showPair(label: string, value: Pair): void { + const copy = { ...value }; + console.log(label, Object.keys(value).join(), JSON.stringify(copy), Object.keys(copy).join()); +} + +function showRow(label: string, value: Row): void { + const copy = { ...value }; + console.log(label, Object.keys(value).join(), JSON.stringify(copy), Object.keys(copy).join()); +} + +// A replaceable method whose replacement returns a record JavaScript builds. +const store = new Store(); +showPair("method", store.get()); +showPair("self copy", { ...store.get(), b: 3 }); +install(store, "get"); +showPair("replaced", store.get()); + +// A JavaScript function, checked into a typed slot, calls a native callback +// it receives inside its options record with a record it builds. +const literal: Row = { y: 20, x: 10 }; +showRow("literal", literal); +showRow("self copy", { ...literal, y: 21 }); +const eachRow = (globalThis as typeof globalThis & { __eachRow: (options: Options) => void }) + .__eachRow; +eachRow({ visit: (row: Row) => showRow("checked function", row) }); diff --git a/tests/harness/corpus-costs/7623-key-order-javascript-built-records/main.ts.json b/tests/harness/corpus-costs/7623-key-order-javascript-built-records/main.ts.json new file mode 100644 index 000000000..4ffc07c5a --- /dev/null +++ b/tests/harness/corpus-costs/7623-key-order-javascript-built-records/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6182, "san": 10202 } From 0e10a01386833d8b9e4d9737c43026bf7500f980 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 07:31:40 -0700 Subject: [PATCH 16/45] Keep record key order through typed iterators and generator resumes Records read out of a typed Map through an `IterableIterator` method (`all() { return this.rows.values(); }`) or out of a `Generator` came back through checked conversions, so the key-order witness treated their type as built at run time and a self copy (`{ ...current, ...patch }`) fell back to the declared order. - A record yield channel keeps its type beside the defaulted `any` return channel, like number, boolean and string already do: for-of and spread read the yielded record unchecked. - `m.values()`/`keys()`/`entries()` on a typed Map or Set flowing into an iterator-typed slot walks the collection with typed reads. It keeps the native iterator's protocol: lazy, live, and parked instead of closed by an early loop exit. - The witness counts a generator resume as building only its IteratorResult record, not the value it carries. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/iterator-adapters.ts | 171 +++++++++++++++++- .../lowering/key-order-witness.test.ts | 35 ++++ .../frontend/lowering/key-order-witness.ts | 17 +- packages/compiler/src/frontend/type-mapper.ts | 9 +- .../7746-key-order-iterable-iterator.ts.json | 6 + .../7746-key-order-iterable-iterator.ts | 58 ++++++ .../7746-key-order-iterable-iterator.ts.json | 1 + 7 files changed, 289 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json create mode 100644 tests/corpus/7746-key-order-iterable-iterator.ts create mode 100644 tests/harness/corpus-costs/7746-key-order-iterable-iterator.ts.json diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index e50652182..a8b4ea496 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -28,11 +28,180 @@ export function iteratorSlotAdapter( expr: IrExpr, expected: IrType & { kind: "generator" }, ): IrExpr | null { - if (expr.type.kind === "dyn") return dynIteratorAdapter(lowerer, expr, expected); + if (expr.type.kind === "dyn") + return ( + collectionIteratorSlotValue(lowerer, expr, expected) ?? + dynIteratorAdapter(lowerer, expr, expected) + ); if (expr.type.kind === "generator") return generatorRetypeAdapter(lowerer, expr, expected); return null; } +/** `m.values()` (or keys/entries, or a Set's) on a typed Map or Set, which + * lowers to a dynamic invocation on the boxed collection, flowing into an + * iterator-typed slot (`values(): IterableIterator { return + * this.rows.values(); }`): an interned generator over the collection that + * reads each entry typed, instead of the checked adapter over the boxed + * native iterator (dynIteratorAdapter), which boxes every value and checks + * it back. It keeps that adapter's protocol: the walk is lazy and live, and + * since the native iterator has no return(), an early exit from a + * consumer's loop parks it on the current entry instead of completing it, + * so a later loop continues with the next entry. Null for any other source + * or a slot whose yield type is not the collection's entry type. */ +function collectionIteratorSlotValue( + lowerer: Lowerer, + expr: IrExpr, + expected: IrType & { kind: "generator" }, +): IrExpr | null { + if (expr.kind !== "dynInvoke" || expr.args.length > 0 || expr.calleeNameValue) return null; + const method = expr.method; + if (method !== "values" && method !== "keys" && method !== "entries") return null; + if (expr.recv.kind !== "dynFrom") return null; + const receiver = expr.recv.value; + const collT = receiver.type; + if (collT.kind !== "map" && collT.kind !== "set") return null; + if (expected.async || expected.yieldT.kind === "void") return null; + if (expected.retT.kind !== "void" && expected.retT.kind !== "dyn") return null; + if (expected.nextT.kind !== "undefinedT" && expected.nextT.kind !== "dyn") return null; + const resultType = genResultRecord( + expected.yieldT, + expected.retT, + lowerer.shapes, + lowerer.unions, + ); + if (!resultType) return null; + const loc = expr.loc; + const key = `collIterSlot:${method}:${typeKey(collT)}:${typeKey(expected)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const coll = varRef("coll.0", collT, loc); + const index = varRef("index.0", F64, loc); + const native = varRef("native.0", DYN, loc); + const resumedFlag = varRef("resumed.0", BOOL, loc); + const keyT = collT.kind === "map" ? collT.key : collT.elem; + const valueT = collT.kind === "map" ? collT.value : collT.elem; + const read = (m: "iterKey" | "iterValue", t: IrType): IrExpr => + collT.kind === "map" + ? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc } + : { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc }; + const entry = (): IrExpr => + method === "keys" || (collT.kind === "set" && method === "values") + ? read("iterKey", keyT) + : method === "values" + ? read("iterValue", valueT) + : { + kind: "recordLit", + fields: [ + { name: "0", value: read("iterKey", keyT) }, + { name: "1", value: read("iterValue", valueT) }, + ], + type: expected.yieldT, + loc, + }; + if (!typeEquals(entry().type, expected.yieldT)) return null; + const suspend = (): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "yieldExpr", value: entry(), type: resumeType(expected.nextT), loc }, + loc, + }); + const finish: IrStmt = { + kind: "return", + value: expected.retT.kind === "dyn" ? dynUndefinedExpr(loc) : null, + loc, + }; + name = keyedHelperName(lowerer, "%coll.iterSlot", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "coll.0", name: "coll", type: collT }], + returnType: expected.retT, + locals: [ + { id: "coll.0", name: "coll", type: collT, mutable: false }, + { id: "native.0", name: "native", type: DYN, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + { id: "resumed.0", name: "resumed", type: BOOL, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "native.0", + init: { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(coll, DYN), + method: "values", + calleeName: collT.kind === "map" ? "map.values" : "set.values", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: "resumed.0", + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "assign", + localId: "index.0", + value: { + kind: "libCall", + fn: "dyn.collectionIteratorIndex", + args: [native], + type: F64, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: index, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: [finish], + else_: null, + loc, + }, + setResumed(false, loc), + { + kind: "tryCatch", + tryBody: [suspend(), setResumed(true, loc)], + catchBody: null, + catchLocalId: null, + // An early exit parks on the current entry; the next step + // resumes the walk after it. + finallyBody: [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: resumedFlag, type: BOOL, loc }, + then: [suspend(), { kind: "continue", loc }], + else_: null, + loc, + }, + ], + loc, + }, + ], + loc, + }, + ], + generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType, tag: "!" }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type: expected, loc }; +} + /** `resumed.0 = value` — the adapters' early-exit marker around a yield. */ function setResumed(value: boolean, loc: SrcLoc): IrStmt { return { diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index 8058d0066..733888763 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -838,3 +838,38 @@ test("a checked function passes its record parameters out, and its callbacks' ba "b", ]); }); + +test("a generator resume builds only its result record, never the value it carries", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const result = f.record( + [ + ["done", { kind: "bool" }], + ["value", ab], + ], + ["value", "done"], + ); + const gen: IrType = { + kind: "generator", + yieldT: ab, + retT: { kind: "void" }, + nextT: { kind: "undefinedT" }, + }; + const resume = (mode: "next" | "value", type: IrType): IrStmt => + observe({ kind: "genResume", mode, gen: ref("g", gen), arg: null, type, loc }); + f.run([ + observe(f.literal(ab, ["b", "a"])), + observe(f.literal(result, ["done", "value"])), + resume("value", ab), + resume("next", result), + ]); + expect(f.order(ab)).toEqual(["b", "a"]); + // The result record itself is built in its fixed layout: today's table. + expect(f.order(result)).toEqual(["value", "done"]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 933c9efcd..4a0a1309f 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -506,12 +506,23 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio case "strIntrinsic": case "regexIntrinsic": case "bytesIntrinsic": - // Generator results ({ done, value }) and Promise.withResolvers - // records are built by the backend in a fixed layout. - case "genResume": + // Promise.withResolvers records are built by the backend in a fixed + // layout. case "promiseWithResolvers": producedBy(e.type, true); return; + case "genResume": { + // A resume builds at most its IteratorResult record ({ done, value } + // in a fixed layout; an async generator's settles a promise of it). + // The value it carries is the generator's own yielded or returned + // value, which the generator's body constructs. The record-free + // drives build nothing: `value` moves that value out, and step, + // close and a record-free delegate answer a boolean or nothing. + if (e.mode === "step" || e.mode === "value" || e.mode === "close") return; + const result = e.type.kind === "promise" ? e.type.inner : e.type; + if (result.kind === "record") opaqueShapes.add(result.shapeId); + return; + } case "dynCheck": if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); // Conversions that build no record: a boxed primitive, and a diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 8adab0488..24a684078 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1389,7 +1389,7 @@ function genChannels( : mapType(nextTs, ctx); if (!nextT || nextT.kind === "void" || nextT.kind === "nullT") return null; const ownChannel = (t: IrType): boolean => - t.kind === "f64" || t.kind === "bool" || t.kind === "string"; + t.kind === "f64" || t.kind === "bool" || t.kind === "string" || t.kind === "record"; const dynMix = (yieldT.kind === "dyn" && retT.kind !== "dyn" && retT.kind !== "void" && !ownChannel(retT)) || (retT.kind === "dyn" && yieldT.kind !== "dyn" && yieldT.kind !== "void" && !ownChannel(yieldT)); @@ -3954,9 +3954,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // argument — fenced at the call). // Mixed dyn/concrete value channels share checked storage, so either // branch of the result record has the same representation. A number, - // boolean or string channel keeps its own type beside a dyn one (the - // `Generator` defaults): its values ride OUT typed and box only - // when the result record is built. + // boolean, string or record channel keeps its own type beside a dyn one + // (the `Generator` and `IterableIterator` defaults): its + // values ride OUT typed and box only when the result record is built, so + // for-of and spread read them back unchecked. if ( isStdlibInterface("Generator") || isStdlibInterface("AsyncGenerator") || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json new file mode 100644 index 000000000..821fa4f21 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7746-key-order-iterable-iterator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7746-key-order-iterable-iterator.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7746-key-order-iterable-iterator.ts b/tests/corpus/7746-key-order-iterable-iterator.ts new file mode 100644 index 000000000..13f27ee83 --- /dev/null +++ b/tests/corpus/7746-key-order-iterable-iterator.ts @@ -0,0 +1,58 @@ +// Records that leave a typed collection through an `IterableIterator` +// method or a `Generator` keep the order their literals create (title +// before id), including copies that spread a value of their own type. The +// iterators stay live and hand out the stored objects themselves. +interface Book { + id: string; + title: string; + version: number; +} +type BookDraft = Omit; +type BookPatch = Partial; + +class Repo { + private readonly rows = new Map(); + put(row: T): void { + this.rows.set(row.id, row); + } + get(id: string): T | undefined { + return this.rows.get(id); + } + all(): IterableIterator { + return this.rows.values(); + } + ids(): IterableIterator { + return this.rows.keys(); + } +} + +function* newest(books: readonly Book[]): Generator { + for (let i = books.length - 1; i >= 0; i--) yield books[i]!; +} + +const books = new Repo(); +let n = 0; +function create(draft: BookDraft): Book { + const book: Book = { ...draft, id: "b" + ++n, version: 1 }; + books.put(book); + return book; +} +function update(id: string, patch: BookPatch): Book { + const current = books.get(id)!; + const next: Book = { ...current, ...patch, version: current.version + 1 }; + books.put(next); + return next; +} + +create({ title: "Dune" }); +const live = books.all(); +console.log(JSON.stringify(create({ title: "Emma" }))); +console.log(JSON.stringify(update("b1", { title: "Dune II" }))); +const listed = [...live]; +console.log(JSON.stringify(listed)); +console.log(listed.length, listed[0] === books.get("b1"), [...books.ids()].join()); +for (const book of books.all()) console.log(Object.keys(book).join()); +const ordered = [...newest(listed)]; +console.log(JSON.stringify(ordered), ordered[0] === listed[1]); +const step = newest(listed).next(); +console.log(step.done, JSON.stringify(step.value)); diff --git a/tests/harness/corpus-costs/7746-key-order-iterable-iterator.ts.json b/tests/harness/corpus-costs/7746-key-order-iterable-iterator.ts.json new file mode 100644 index 000000000..de3f40849 --- /dev/null +++ b/tests/harness/corpus-costs/7746-key-order-iterable-iterator.ts.json @@ -0,0 +1 @@ +{ "plain": 3800, "san": 6100 } From bd36b77a8fff8a36550647208de65629b21cb3f0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 11:57:22 -0500 Subject: [PATCH 17/45] Cache untyped property access in programs that declare classes (reland) --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/dynamic-properties.json | 6 + .../runtime/workloads/dynamic-properties.mjs | 56 ++++++++ packages/compiler/src/ir/js-ic.test.ts | 134 ++++++++++++++++++ packages/compiler/src/ir/js-ic.ts | 33 ++++- packages/compiler/test/js-ic-emission.test.ts | 44 ++++++ packages/runtime/src/scr_js_object.c | 32 ++++- scripts/zero-cost-compare.mjs | 5 +- tests/harness/library-mode.test.ts | 20 +++ tests/library-mode/js-ic-sessions/lib.ts | 6 + tests/library-mode/js-ic-sessions/probe.c | 25 ++++ .../library-mode/js-ic-sessions/profile.json | 14 ++ tests/library-mode/js-ic-sessions/work.mjs | 26 ++++ 13 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 benchmarks/runtime/workloads/dynamic-properties.json create mode 100644 benchmarks/runtime/workloads/dynamic-properties.mjs create mode 100644 tests/library-mode/js-ic-sessions/lib.ts create mode 100644 tests/library-mode/js-ic-sessions/probe.c create mode 100644 tests/library-mode/js-ic-sessions/profile.json create mode 100644 tests/library-mode/js-ic-sessions/work.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 4f152e4bb..04c90f7bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -13,6 +13,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | | `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | | `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | +| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | diff --git a/benchmarks/runtime/workloads/dynamic-properties.json b/benchmarks/runtime/workloads/dynamic-properties.json new file mode 100644 index 000000000..398981ef3 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-properties.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-properties.mjs", + "args": [ + "1000000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-properties.mjs b/benchmarks/runtime/workloads/dynamic-properties.mjs new file mode 100644 index 000000000..d0ee3802c --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-properties.mjs @@ -0,0 +1,56 @@ +// Untyped JavaScript reading and writing properties of values it never +// declares types for: records built by object literals in two key orders, +// nested records, property updates, and instances of a class and its +// subclass read and written the same way. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "1000000"); +class Cursor { + constructor(x, y) { + this.x = x; + this.y = y; + } + step(dx, dy) { + this.x += dx; + this.y += dy; + return this; + } +} +class Marker extends Cursor { + constructor(x, y, label) { + super(x, y); + this.label = label; + } +} +function makeItem(i) { + if (i % 3 === 0) + return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } }; + return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } }; +} +function cost(item) { + return item.price * item.qty + item.meta.weight; +} +function restock(item, round) { + item.qty = (item.qty + round) % 7; + item.meta.tag = (item.meta.tag + 1) % 7; +} +function distance(a, b) { + return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); +} +const items = []; +for (let i = 0; i < 1024; i++) items.push(makeItem(i)); +const cursors = []; +for (let i = 0; i < 64; i++) + cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i)); +const origin = { x: 0, y: 0 }; +let checksum = 0; +for (let r = 0; r < rounds; r++) { + const item = items[r & 1023]; + checksum += cost(item); + restock(item, r & 3); + checksum += item.meta.tag; + const cursor = cursors[r & 63]; + checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin); + if (cursor.x > 100 || cursor.x < -100) cursor.x = 0; + if (cursor.y > 100 || cursor.y < -100) cursor.y = 0; +} +console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label); diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index f71a1e9db..026600cc3 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -126,6 +126,140 @@ test("property helpers whose class branches only test capsules are cache sites", expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); }); +test("bags converted between dyn and jsval are still the receiver", () => { + // A program that declares a class returns its bags as jsvals, and the + // helpers read them through js.toDyn. + const toDyn = (value: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "js.toDyn", + args: [value], + type: DYN, + loc, + }); + const fromDyn = (value: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "js.fromDyn", + args: [value], + type: { kind: "jsval" }, + loc, + }); + const jsvalBag = helper( + "%dyn.class.readPropertiesOrEmpty.js", + ["v"], + [capsuleBranch("v"), ret(fromDyn(ref("v")))], + ); + const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]); + const getter = getHelper( + "%dyn.class.property.0", + "x", + [capsuleBranch("p.0")], + call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))), + ); + const setter = setHelper("%dyn.class.property.1", "y"); + const write = setter.body[0] as Extract; + const keySet = write.expr as Extract; + keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0"))); + // A converted value that is not the receiver stays a helper call. + const foreign = getHelper( + "%dyn.class.property.2", + "x", + [], + toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)), + ); + const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); +}); + +test("a bag helper may first rebind its parameter to a bag of it", () => { + // Programs with subclasses normalize the value before the class branches. + const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]); + const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc }); + const normalized = helper( + "%dyn.class.readPropertiesOrEmpty.normalized", + ["v"], + [rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))], + ); + const replaced = helper( + "%dyn.class.readPropertiesOrEmpty.replaced", + ["v"], + [rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))], + ); + const sites = sitesOf( + normalize, + normalized, + replaced, + getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))), + getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))), + // The property helper itself normalizes its receiver first, too. + getHelper("%dyn.class.property.2", "x", [ + { kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc }, + capsuleBranch("p.0"), + ]), + ); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toBeNull(); + expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" }); +}); + +test("keyed helpers may test capsules between the key's coercion and the read", () => { + const read = (object: IrExpr, key: string): IrExpr => ({ + kind: "libCall", + fn: "js.fromDyn", + args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }], + type: { kind: "jsval" }, + loc, + }); + const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({ + kind: "if", + cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc }, + then: [ret(read(object, "p.key"))], + else_: null, + loc, + }); + const keyed = (name: string, between: IrStmt[]): IrFunction => + helper( + name, + ["p.0", "p.key"], + [ + capsuleBranch("p.0"), + guard("symbol", "p.key", call(identityBag.name, ref("p.0"))), + guard("nullish", "p.0", ref("p.0")), + { + kind: "varDecl", + localId: "key.string", + init: { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [ref("p.key")], + type: STRING, + loc, + }, + loc, + }, + ...between, + ret(read(call(identityBag.name, ref("p.0")), "key.string")), + ], + ["key.string"], + ); + const errorBranch: IrStmt = { + kind: "if", + cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc }, + then: [ret(ref("p.0"))], + else_: null, + loc, + }; + const sites = sitesOf( + keyed("%dyn.class.computed.0", []), + keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]), + keyed("%dyn.class.computed.2", [errorBranch]), + ); + expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" }); + expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" }); + expect(sites.site("%dyn.class.computed.2")).toBeNull(); +}); + test("any other prefix or tail keeps the helper call", () => { const errorBranch: IrStmt = { kind: "if", diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 752316db3..58c568bbd 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -62,7 +62,11 @@ export class JsIcSites { const receiver = fn.params[0]?.localId; if (receiver === undefined) return null; let i = 0; - while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++; + while ( + i < fn.body.length && + (this.capsuleBranch(fn.body[i]!, receiver) || this.rebinding(fn.body[i]!, receiver)) + ) + i++; const tail = fn.body.slice(i); if (fn.params.length === 1) return this.getTail(tail, receiver); if (fn.params.length !== 2) return null; @@ -109,10 +113,14 @@ export class JsIcSites { /** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key); * if (receiver == null) return dynKeyGet(receiver, key); * const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read - * possibly under js.fromDyn. */ + * possibly under js.fromDyn. Capsule branches may precede the final read: + * a program that declares classes looks the string key up in each class's + * key table there. */ private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null { - const [symbol, nullish, decl, ret] = tail; - if (tail.length !== 4) return null; + const [symbol, nullish, decl] = tail; + const ret = tail[tail.length - 1]; + if (tail.length < 4) return null; + if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null; if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null; if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key)) return null; @@ -159,9 +167,12 @@ export class JsIcSites { } /** Whether `e` is the receiver itself for every ordinary object or array: - * the receiver, or a chain of property-bag helpers applied to it. */ + * the receiver, or a chain of property-bag helpers applied to it. A + * conversion between the dyn and jsval forms keeps the object it holds. */ private bagOf(e: IrExpr, receiver: string): boolean { if (isVar(e, receiver)) return true; + if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn")) + return e.args.length === 1 && this.bagOf(e.args[0]!, receiver); return ( e.kind === "call" && e.args.length === 1 && @@ -182,6 +193,13 @@ export class JsIcSites { return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); } + /** `param = `, such as the subclass normalization + * `value = %dyn.class.normalize(value)`, which leaves an ordinary object + * or array in `param`. */ + private rebinding(s: IrStmt, param: string): boolean { + return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param); + } + /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -204,7 +222,10 @@ export class JsIcSites { identity = fn.body .slice(0, -1) - .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && + .every( + (s) => + this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param), + ) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 47d641bff..54ea13d26 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -64,6 +64,50 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); } }); +test("property sites keep their caches in programs that declare classes and subclasses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-")); + try { + const entry = join(dir, "main.js"); + const outPath = join(dir, "main.ir.json"); + // Declared classes give every property helper class branches and + // property-bag lookups, and a subclass adds a normalization step; plain + // objects still read through the caches. + await writeFile( + entry, + ` +class Point { + constructor(x) { this.x = x; } +} +class Tagged extends Point { + constructor(x, tag) { super(x); this.tag = tag; } +} +function dyn(v) { return v; } +function read(o) { return o.x; } +function write(o, v) { o.x = v; } +function at(a, i) { return a[i]; } +var o = dyn({ x: 1 }); +var p = dyn(new Point(4)); +var t = dyn(new Tagged(6, "t")); +write(o, 3); +write(p, 5); +write(t, 7); +console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag")); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(mod)).toEqual([]); + const llvm = emitLlvmModule(mod); + expect(body(llvm, "read")).toContain("@scr_js_load_probe("); + expect(body(llvm, "write")).toContain("@scr_js_store_probe("); + expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test("typed programs have no cache sites and keep the ordinary runtime", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-")); try { diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 63929ab57..0e0ec0908 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -18,6 +18,7 @@ #include "scr_key.h" #include +#include /* The compiler's inline cache tests read these in place (64-bit targets; * backend/llvm/js-ic.ts). */ @@ -37,10 +38,10 @@ _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layo * take the generic path and its hash index. */ #define SCR_JS_SHAPE_MAX 64 -/* Shapes live until the thread exits, so a thread creates at most this - * many, holding at most this many key bytes. Once either budget is spent, - * objects whose key sequence has no shape yet stay unshaped; shapes that - * exist keep serving. */ +/* Shapes live until the thread exits or a library session ends, so a thread + * creates at most this many, holding at most this many key bytes. Once + * either budget is spent, objects whose key sequence has no shape yet stay + * unshaped; shapes that exist keep serving. */ #define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16) #define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20) @@ -63,12 +64,25 @@ static SCR_TL size_t scr_js_transition_count; static SCR_TL size_t scr_js_shape_key_bytes; static SCR_TL bool scr_js_shapes_registered; +/* The cache records that hold entries. Entries name shapes and prototype + * objects, so the reset that frees the shapes also clears these records: a + * library's next session runs the same sites with the same records. */ +static SCR_TL ScrJsIc **scr_js_filled_ics; +static SCR_TL size_t scr_js_filled_count; +static SCR_TL size_t scr_js_filled_capacity; + /* The shape word of an object whose key sequence has no shape within the * budgets. No cache entry names it, so its accesses take the probes, * which answer with a miss until the key set changes. */ static ScrJsShape scr_js_unshaped; static void scr_js_shapes_reset(void) { + for (size_t i = 0; i < scr_js_filled_count; i++) + memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]); + free(scr_js_filled_ics); + scr_js_filled_ics = NULL; + scr_js_filled_count = 0; + scr_js_filled_capacity = 0; while (scr_js_all_shapes) { ScrJsShape *shape = scr_js_all_shapes; scr_js_all_shapes = shape->all_next; @@ -192,6 +206,16 @@ static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t /* Fills a cache entry: `stale` (a matching entry whose hit failed) when * there is one, else a free or replaceable one. */ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { + if (ic->count == 0) { + if (scr_js_filled_count == scr_js_filled_capacity) { + size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64; + ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + scr_js_filled_ics = grown; + scr_js_filled_capacity = capacity; + } + scr_js_filled_ics[scr_js_filled_count++] = ic; + } uint32_t index; if (stale < ic->count) { index = stale; diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 160e33438..f202c2ab7 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,8 +394,8 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. bench/dynamic-methods is - * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/dynamic-methods and + * bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -410,6 +410,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/thread-slots", "bench/channel-messages", "bench/dynamic-methods", + "bench/dynamic-properties", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index db4fabb3e..b60c3ac54 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -363,6 +363,17 @@ globals: undefined fenced expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); + test("K4: untyped property caches start empty in every session", async () => { + // The shapes a session's cache entries name are freed when it ends; + // the next session's sites must not find those entries. + const { archive, outDir } = await buildLibrary("js-ic-sessions", emission); + const probe = buildProbe("js-ic-sessions", archive, outDir); + const run = runProbe(probe); + expect(run.signal).toBeNull(); + expect(run.status).toBe(0); + expect(run.stdout).toBe("run: 20000\n".repeat(3)); + }); + /* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */ test("K5: a trap delivers to the sink exactly once, host survives", async () => { @@ -582,6 +593,15 @@ survived, sink_calls=1 expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); + platformTest("K10: property caches across sessions under ASan", async () => { + const { archive, outDir } = await buildLibrary("js-ic-sessions", emission, { sanitize: true }); + const probe = buildProbe("js-ic-sessions", archive, outDir, { sanitize: true }); + const run = runProbe(probe); + expect(run.signal).toBeNull(); + expect(run.status).toBe(0); + expect(run.stdout).toBe("run: 20000\n".repeat(3)); + }); + platformTest("K10: K5/K7 under ASan", async () => { const { archive, outDir } = await buildLibrary("traps", emission, { sanitize: true }); const probe = buildProbe("traps", archive, outDir, { sanitize: true }); diff --git a/tests/library-mode/js-ic-sessions/lib.ts b/tests/library-mode/js-ic-sessions/lib.ts new file mode 100644 index 000000000..ebea59a54 --- /dev/null +++ b/tests/library-mode/js-ic-sessions/lib.ts @@ -0,0 +1,6 @@ +// The library's typed export over untyped code (work.mjs). +import { runAll } from "./work.mjs"; + +export function run(): number { + return runAll() as number; +} diff --git a/tests/library-mode/js-ic-sessions/probe.c b/tests/library-mode/js-ic-sessions/probe.c new file mode 100644 index 000000000..7dbcb56bf --- /dev/null +++ b/tests/library-mode/js-ic-sessions/probe.c @@ -0,0 +1,25 @@ +/* Three sessions of a library whose untyped code reads and writes through + * property caches: each prints the same result, and under the sanitize + * flavor no session reads a cache entry the previous session freed. */ +#include +#include + +extern void kj_init(void); +extern void kj_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx); +extern void kj_collect(void); +extern double kj_run(void); + +static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { + (void)ctx; (void)addr; + printf("UNEXPECTED SINK: %.*s", (int)len, (const char *)msg); +} + +int main(void) { + kj_set_panic_sink(sink, NULL); + for (int session = 0; session < 3; session++) { + kj_init(); + printf("run: %.0f\n", kj_run()); + kj_collect(); + } + return 0; +} diff --git a/tests/library-mode/js-ic-sessions/profile.json b/tests/library-mode/js-ic-sessions/profile.json new file mode 100644 index 000000000..fd802b42c --- /dev/null +++ b/tests/library-mode/js-ic-sessions/profile.json @@ -0,0 +1,14 @@ +{ + "profile_format": 1, + "name": "conformance-js-ic-sessions", + "entry": "lib.ts", + "emission": "llvm", + "abi": { + "prefix": "kj_", + "init_symbol": "kj_init", + "sink_register_symbol": "kj_set_panic_sink", + "collect_symbol": "kj_collect", + "result_reset_symbol": null + }, + "exports": [{ "export": "run", "symbol": "kj_run", "params": [], "returns": "f64" }] +} diff --git a/tests/library-mode/js-ic-sessions/work.mjs b/tests/library-mode/js-ic-sessions/work.mjs new file mode 100644 index 000000000..5c1afcd18 --- /dev/null +++ b/tests/library-mode/js-ic-sessions/work.mjs @@ -0,0 +1,26 @@ +// Dynamic-tier property caches across library sessions: objects built empty +// and filled through untyped stores, read back through untyped loads, and a +// class instance at the same kind of sites. Each session must start with +// empty caches, since the shapes their entries named are freed when the +// previous session ends. +class Box { + constructor(v) { + this.v = v; + } +} +function fill(o, n) { + o.a = n; + o.b = n + 1; + return o; +} +function sum(o) { + return o.a + o.b; +} +function unbox(b) { + return b.v; +} +export function runAll() { + let total = 0; + for (let i = 0; i < 100; i++) total += sum(fill({}, i)); + return total + unbox(new Box(total)); +} From 7fb73b7d9da248acc4815d1c03646d83316dc861 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 10:05:54 -0700 Subject: [PATCH 18/45] Keep the key order of records spread from values JavaScript built A literal that starts with `...value` creates value's keys first, in value's own order. When checked conversions build value's shape (records JavaScript hands back through replaceable methods, field setters, generator calls or callbacks), the key-order witness could not order the literal statically and kept the declared table, so `{ ...draft, id }` and `{ ...current, ...patch }` enumerated `id` first natively. Every value of a record shape already enumerates either in its table or in the order its record source holds: a checked conversion keeps the source's order there whenever it differs from the table. Such a literal (an ordered spread) now carries that order over: after it builds the record, record.sourceSpread gives the record a source listing the keys it declares in value's order, and the keys value lacks follow in the table's order, which every literal sequence of the shape agrees with. The literal then resolves as if value were in its table's order, and its own shape counts as built at run time, since its values may carry a source too. It qualifies only when every value of the source has all of the source's keys, both shapes can keep a source, every later spread of a shape whose values may carry a source only overwrites the source's keys, and the source shape resolves (or is the literal's own shape). Literals of shapes that do not resolve are left as they were. Programs without such literals compile to identical IR. Corpus 7750-key-order-javascript-spread-sources. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/dyn.ts | 13 + .../compiler/src/backend/llvm/lib-dispatch.ts | 9 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/key-order-witness.test.ts | 90 +++++++ .../frontend/lowering/key-order-witness.ts | 234 +++++++++++++++--- .../compiler/src/ir/builtin-signatures.ts | 5 + .../main.ts.json | 7 + packages/runtime/src/scr_dyn.h | 6 + packages/runtime/src/scr_json.c | 32 +++ .../lib.js | 14 ++ .../main.ts | 105 ++++++++ .../main.ts.json | 1 + 12 files changed, 489 insertions(+), 28 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json create mode 100644 tests/corpus/7750-key-order-javascript-spread-sources/lib.js create mode 100644 tests/corpus/7750-key-order-javascript-spread-sources/main.ts create mode 100644 tests/harness/corpus-costs/7750-key-order-javascript-spread-sources/main.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 94581cfa0..2234b8db2 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1505,6 +1505,19 @@ export class LlDyn { return descriptor; } + /** `record.sourceSpread(built, value)`: the built record keeps value's + * kept-source order over its own declared names. */ + emitRecordSourceSpread(B: BlockBuilder, built: string, value: string, shapeId: string): void { + const host = this.host; + const shape = host.recordsById.get(shapeId)!; + this.keepsRecordSources = true; + const names = this.recordNameTable(shapeId, shape); + host.declare(`declare void @scr_record_source_spread(ptr, ptr, ptr, ptr, ${host.sizeType})`); + B.line( + `call void @scr_record_source_spread(ptr ${built}, ptr ${value}, ptr ${names.names}, ptr ${names.lens}, ${host.sizeType} ${shape.fields.length})`, + ); + } + /** The declared-name tables scr_record_source_note scans: parallel * constant arrays of field-name bytes and byte lengths in declaration * order (the order the record serializes), interned per shape. Internal diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..c8fae3192 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -236,6 +236,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll host.emitPendingCheck(); return output; } + if (e.fn === "record.sourceSpread") { + const built = host.emitReadReceiver(e.args[0]!); + const value = host.emitReadReceiver(e.args[1]!); + const type = e.args[0]!.type; + if (type.kind !== "record") + throw new InternalCompilerError("llvm emitter bug: record.sourceSpread of a non-record"); + host.dyn.emitRecordSourceSpread(B, built.name, value.name, type.shapeId); + return { name: "", type: e.type }; + } if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") { const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null; if (e.fn === "dyn.typedRefKeyIs" && literal === null) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7dd4370c5..13f9fcdc5 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -676,6 +676,7 @@ export const LIB_FN_SYMS: Record = { "insp.circular": "scr_insp_circular", // Undeclared source properties kept beside a checked record (scr_json.c). "record.sourceHas": "scr_record_source_has", + "record.sourceSpread": "scr_record_source_spread", // WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c): // constructions +1; url.new, the fileURLToPath pair, the win32 // pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw). diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index 733888763..d68be96a8 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -42,6 +42,7 @@ function fixture(): Fixture { unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]), classes: new Map(), methodAccessSites: new Map(), + dynConvertible: () => true, armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ @@ -873,3 +874,92 @@ test("a generator resume builds only its result record, never the value it carri // The result record itself is built in its fixed layout: today's table. expect(f.order(result)).toEqual(["value", "done"]); }); + +/** `{ a, b }` built by a checked conversion and by the literal `{ b, a }`, + * copied by `{ ...value, a }` (evaluating the value into a binding, as + * spread literals do), observed. Answers the table and the function. */ +function checkedSelfCopy(): { order: string[]; fn: IrFunction; copy: IrExpr } { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const value = ref("value", ab) as IrExpr & { kind: "varRef" }; + const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false, value }; + const literal = f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }]); + const copy: IrExpr = { + kind: "seqExpr", + stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })], + result: literal, + type: ab, + loc, + }; + const fn = f.run([observe(f.literal(ab, ["b", "a"])), observe(copy)]); + return { order: f.order(ab)!, fn, copy }; +} + +test("a spread of a value a checked conversion built carries the value's order over", () => { + const { order, fn, copy } = checkedSelfCopy(); + // Values the literal builds enumerate in its order; copies of converted + // values keep the order their record source holds. + expect(order).toEqual(["b", "a"]); + expect(copy).toMatchObject({ + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: "value" }, + { kind: "varDecl", localId: "%orderedSpread.0", init: { kind: "recordLit" } }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "record.sourceSpread", + args: [ + { kind: "varRef", localId: "%orderedSpread.0" }, + { kind: "varRef", localId: "value" }, + ], + }, + }, + ], + result: { kind: "varRef", localId: "%orderedSpread.0" }, + }); + expect(fn.locals.map((local) => local.id)).toEqual(["%orderedSpread.0"]); +}); + +test("a spread of a converted value into a wider shape orders the wider shape", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const abc = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["c", "a", "b"], + ); + const value = ref("value", ab) as IrExpr & { kind: "varRef" }; + const spread: KeyOrderSegment = { + kind: "spread", + shapeId: ab.shapeId, + maybeAbsent: false, + value, + }; + const wide: IrExpr = { + kind: "seqExpr", + stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })], + result: f.literal(abc, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }]), + type: abc, + loc, + }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(wide)]); + expect(f.order(ab)).toEqual(["b", "a"]); + expect(f.order(abc)).toEqual(["b", "a", "c"]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 4a0a1309f..6e65d6f6d 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -41,6 +41,12 @@ * - a literal that starts by spreading a value of its own shape copies * that value's order. While no checked conversion builds the shape, * every value has the one chosen order, so the copy adds no witness; + * - a literal that starts by spreading a value of a shape checked + * conversions build (an ordered spread) gives the record it builds the + * value's record source, when the value has one (record.sourceSpread). + * Every value of a shape enumerates in its table or in the order its + * record source holds, so the copy has the value's order either way, + * and the literal resolves as if the value were in its table's order; * - a write to an optional key that may be absent appends it in Node. A * key no construction creates ("late-only") goes after every key a * construction can create; any other moving write goes after every key. @@ -61,6 +67,7 @@ import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T, + VOID, } from "../../ir/ir.js"; import { everyExprChild, @@ -71,6 +78,7 @@ import { import type { Lowerer } from "./lowerer.js"; import * as ts from "../ts7/adapter.js"; import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; +import { recordMayKeepSource } from "./record-sources.js"; /** The statements a key-order helper runs for record `ref`: `walks[0]`, * its walk over the shape's table, or, for a shape with orderVariants, a @@ -218,6 +226,14 @@ interface ShapeFacts { type Resolved = { order: string[] } | "unknown"; +/** A literal that starts with `...value`: the sequence that evaluates the + * value into its binding and then builds the literal, and its function. */ +interface LeadingSpread { + wrapper: IrExpr & { kind: "seqExpr" }; + literal: IrExpr & { kind: "recordLit" }; + fn: IrFunction; +} + /** `structuredClone(v)` checked back into v's own type: the copy is built * from v's dyn view, so it creates exactly v's keys in v's order. */ function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { @@ -412,6 +428,12 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // ── collection ────────────────────────────────────────────────────── let pos = 0; let fnName = ""; + let currentFn: IrFunction | null = null; + /** Literals that start by spreading a binding, by their witness: where + * each one is built. A literal's notes are shared by its rebuilt copies, + * so `literalSites` counts every literal of each witness. */ + const leadingSpreads = new Map(); + const literalSites = new Map(); const loops: number[] = []; let loopIds = 0; const loopKey = (): string => loops.join("/"); @@ -463,8 +485,10 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio } const segments = notes.literalOf(e); const copy = notes.copyOf(e); - if (segments !== undefined) f.literals.push(segments); - else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); + if (segments !== undefined) { + f.literals.push(segments); + literalSites.set(segments, (literalSites.get(segments) ?? 0) + 1); + } else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); else f.pinned = true; return; } @@ -479,6 +503,18 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio case "assignExpr": noteInit(e.localId, e.value); return; + case "seqExpr": { + // A spread literal evaluates its sources into bindings first. + const literal = e.result; + if (literal.kind !== "recordLit" || currentFn === null) return; + const segments = notes.literalOf(literal); + const first = segments?.[0]; + if (segments === undefined || first?.kind !== "spread" || first.value === undefined) return; + const list = leadingSpreads.get(segments) ?? []; + list.push({ wrapper: e, literal, fn: currentFn }); + leadingSpreads.set(segments, list); + return; + } case "jsonStringify": case "dynFrom": observedRoots.push(e.value.type); @@ -582,6 +618,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio }; for (const fn of fns) { fnName = fn.name; + currentFn = fn; for (const st of fn.body) stmt(st); } for (const id of opaqueShapes) { @@ -698,6 +735,61 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio return edges; }; + // ── ordered spreads ───────────────────────────────────────────────── + // A value a checked conversion builds keeps its source's key order in + // its record source when that order is not the table's (record- + // sources.ts), so every value of a shape enumerates in the table's order + // or in the order its record source holds. A literal that starts with + // `...value` creates the value's keys first, in the value's order. When + // the value's shape is one checked conversions build, the literal + // carries that order over: after it builds the record it gives the + // record a source holding the value's order (record.sourceSpread), and + // the keys the value lacks follow in the table's order, which every + // literal sequence of the shape agrees with. Such a literal resolves as + // if its source were in the source's table order, and its own shape + // counts as built at run time too, since its values may now carry a + // source. It qualifies when every value of the source has all of the + // source's keys, both shapes can keep a source, and every later spread + // whose values may carry a source only overwrites the source's keys + // (its own order cannot move them). + const orderedSpreads = new Set(); + const keepsSource = (shapeId: string): boolean => + recordMayKeepSource(lowerer, { kind: "record", shapeId }); + const userKeys = (shapeId: string): string[] => + (shapes.get(shapeId)?.fields ?? []).map((x) => x.name).filter((k) => !k.startsWith("%")); + const ordersSpread = (segments: KeyOrderSegment[], sites: readonly LeadingSpread[]): boolean => { + const first = segments[0]; + if (first?.kind !== "spread" || first.maybeAbsent || first.value === undefined) return false; + const sourceId = first.shapeId; + if (facts.get(sourceId)?.runtime !== true || sites.length !== literalSites.get(segments)) + return false; + const target = sites[0]!.literal.type; + if (target.kind !== "record" || !keepsSource(sourceId) || !keepsSource(target.shapeId)) + return false; + const localId = first.value.localId; + const bound = ({ wrapper, literal }: LeadingSpread): boolean => + typeEquals(literal.type, target) && + wrapper.stmts.some( + (st) => + st.kind === "varDecl" && + st.localId === localId && + st.init !== null && + st.init.type.kind === "record" && + st.init.type.shapeId === sourceId, + ); + if (!sites.every(bound)) return false; + const sourceKeys = new Set(userKeys(sourceId)); + if ([...sourceKeys].some((k) => maybeAbsent(sourceId, k))) return false; + return segments + .slice(1) + .every( + (s) => + s.kind === "key" || + (s.kind === "spread" && + (facts.get(s.shapeId)?.runtime !== true || + userKeys(s.shapeId).every((k) => sourceKeys.has(k)))), + ); + }; // ── witness sequences ─────────────────────────────────────────────── /** The key sequences one literal can create, or null when a spread * source's order is unknown. A key sits at its first occurrence that @@ -707,15 +799,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio segments: readonly KeyOrderSegment[], keySet: ReadonlySet, resolved: (id: string) => Resolved, + raw: (id: string) => Resolved, ): string[][] | null => { const occurrences: { key: string; definite: boolean }[] = []; + const ordered = orderedSpreads.has(segments); for (const s of segments) { if (s.kind === "opaque") return null; if (s.kind === "key") { if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite }); continue; } - const source = resolved(s.shapeId); + // An ordered spread's values without a source enumerate in the + // source's table; the others carry their order over. + const source = ordered && s === segments[0] ? raw(s.shapeId) : resolved(s.shapeId); if (source === "unknown") return null; for (const k of source.order) { if (keySet.has(k)) @@ -809,13 +905,17 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio first.kind === "spread" && first.shapeId === id && !first.maybeAbsent && - !f.runtime && + (!f.runtime || orderedSpreads.has(segments)) && segments.every((s) => s.kind !== "opaque") && table.every((k) => !maybeAbsent(id, k)) ); }; /** One shape's order given its sources' current orders. */ - const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { + const resolveOne = ( + id: string, + current: (source: string) => Resolved, + raw: (source: string) => Resolved, + ): Resolved => { const shape = shapes.get(id); if (!inScope(shape)) return "unknown"; const table = tableOf(shape); @@ -832,7 +932,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio if (f.pinned) chain(table, edges); for (const segments of f.literals) { if (copiesItself(id, f, table, segments)) continue; - const sequences = literalSequences(segments, keySet, current); + const sequences = literalSequences(segments, keySet, current, raw); if (sequences === null) return "unknown"; for (const seq of sequences) chain(seq, edges); } @@ -891,32 +991,57 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const known = result.get(id); return known === undefined ? "unknown" : asSource(id, known); }; - for (const scc of sccs) { - const members = new Set(scc); - const guess = new Map(); - for (const id of scc) { - const shape = shapes.get(id); - guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); - } - const lookup = (id: string): Resolved => - members.has(id) ? asSource(id, guess.get(id)!) : external(id); - let stable = false; - for (let round = 0; round < scc.length + 3 && !stable; round++) { - stable = true; + const resolveAll = (): void => { + result.clear(); + late.clear(); + for (const scc of sccs) { + const members = new Set(scc); + const guess = new Map(); for (const id of scc) { - const next = resolveOne(id, lookup); - const prev = guess.get(id)!; - const same = - next === "unknown" - ? prev === "unknown" - : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); - if (!same) { - stable = false; - guess.set(id, next); + const shape = shapes.get(id); + guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); + } + const lookup = (id: string): Resolved => + members.has(id) ? asSource(id, guess.get(id)!) : external(id); + const rawLookup = (id: string): Resolved => + members.has(id) ? guess.get(id)! : (result.get(id) ?? "unknown"); + let stable = false; + for (let round = 0; round < scc.length + 3 && !stable; round++) { + stable = true; + for (const id of scc) { + const next = resolveOne(id, lookup, rawLookup); + const prev = guess.get(id)!; + const same = + next === "unknown" + ? prev === "unknown" + : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); + if (!same) { + stable = false; + guess.set(id, next); + } } } + for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); } - for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); + }; + // Ordered spreads join once their source resolves; each one can let its + // own shape resolve, which a later spread of it needs. + resolveAll(); + for (let grew = true; grew;) { + grew = false; + for (const [segments, sites] of leadingSpreads) { + const first = segments[0]; + if (first?.kind !== "spread" || orderedSpreads.has(segments)) continue; + // A self copy resolves with its own shape. + const target = sites[0]!.literal.type; + const source = result.get(first.shapeId) ?? "unknown"; + const self = target.kind === "record" && target.shapeId === first.shapeId; + if ((source === "unknown" && !self) || !ordersSpread(segments, sites)) continue; + orderedSpreads.add(segments); + if (target.kind === "record") factsOf(target.shapeId).runtime = true; + grew = true; + } + if (grew) resolveAll(); } // ── observation ───────────────────────────────────────────────────── @@ -925,6 +1050,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio for (const id of notes.observed) shapesIn({ kind: "record", shapeId: id }, observed, observedSeen); for (const root of observedRoots) shapesIn(root, observed, observedSeen); + // An ordered spread's copies of values without a source take the source's + // order from its table, which must then hold the resolved order. + const orderedTarget = (sites: readonly LeadingSpread[]): string | null => { + const target = sites[0]!.literal.type; + return target.kind === "record" && (result.get(target.shapeId) ?? "unknown") !== "unknown" + ? target.shapeId + : null; + }; + for (const [segments, sites] of leadingSpreads) { + const first = segments[0]; + if (orderedSpreads.has(segments) && first?.kind === "spread" && orderedTarget(sites) !== null) + observed.add(first.shapeId); + } // ── apply ─────────────────────────────────────────────────────────── for (const [id, resolved] of result) { @@ -937,6 +1075,46 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio notes.changed.add(id); } + // Ordered spreads carry their source's order over (see above). + let orderedResults = 0; + const rewritten = new Set(); + for (const [segments, sites] of leadingSpreads) { + const first = segments[0]; + if (!orderedSpreads.has(segments) || first?.kind !== "spread" || first.value === undefined) + continue; + // A shape that did not resolve keeps today's table for every value. + if (orderedTarget(sites) === null) continue; + for (const { wrapper, literal, fn } of sites) { + if (rewritten.has(wrapper)) continue; + rewritten.add(wrapper); + const loc = literal.loc; + const localId = `%orderedSpread.${orderedResults++}`; + fn.locals.push({ id: localId, name: "%orderedSpread", type: literal.type, mutable: false }); + const built: IrExpr = { kind: "varRef", localId, type: literal.type, loc }; + const source: IrExpr = { + kind: "varRef", + localId: first.value.localId, + type: { kind: "record", shapeId: first.shapeId }, + loc, + }; + wrapper.stmts.push( + { kind: "varDecl", localId, init: literal, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "record.sourceSpread", + args: [built, source], + type: VOID, + loc, + }, + loc, + }, + ); + wrapper.result = { kind: "varRef", localId, type: literal.type, loc }; + } + } + // ── per-instance orders ───────────────────────────────────────────── // A shape whose literals write its keys in more than one order has no // one table. Each value then records which of the orders it was written diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6a645785f..9ea65d47d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4424,6 +4424,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * (inspect, Object.keys/values/entries) then answer through its live * checked view. The receiver is any such record type. Never throws. */ "record.sourceHas": { argTypes: [null], result: BOOL }, + /** `record.sourceSpread(built, value)`: the record a literal starting + * with `...value` just built keeps the order of value's kept source + * (key-order-witness.ts ordered spreads). Both are record types. Never + * throws. */ + "record.sourceSpread": { argTypes: [null, null], result: VOID }, "insp.key": { argTypes: [STRING], result: STRING }, "insp.moreItems": { argTypes: [F64], result: STRING }, "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json new file mode 100644 index 000000000..19f1ba620 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-key-order-javascript-spread-sources/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7750-key-order-javascript-spread-sources/lib.js", + "/tests/corpus/7750-key-order-javascript-spread-sources/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..c3a6a8f38 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1020,6 +1020,12 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names, const size_t *lens, size_t count); bool scr_record_source_has(const void *rec); +/* A literal that starts with `...src` creates src's keys first, in src's + * order: when src kept a source, the fresh record `dst` (whose shape's + * declared names are `names`) keeps one listing the keys it declares in + * that order. Undeclared keys are not copied, as the literal copies none. */ +void scr_record_source_spread(void *dst, const void *src, const char *const *names, + const size_t *lens, size_t count); /* The commit of a live record view converts the view into a new record * `from`, whose conversion keeps the view's undeclared properties, then * moves its fields into `to`: this moves the kept properties to `to` too, diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..28ce987ff 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3191,6 +3191,38 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam scr_record_source_put(&probe); } +void scr_record_source_spread(void *dst, const void *src, const char *const *names, + const size_t *lens, size_t count) { + if (!scr_record_source_count || !dst || !src) return; + ScrRecordSource *found = scr_record_source_find(src); + if (!found) return; + ScrRecordSource probe = { dst, NULL, (const char *)names, (const char *)lens, + sizeof *names, sizeof *lens, count }; + /* Nothing to keep when the declared keys come in the table's order and + * the keys src lacks would follow them in it anyway. */ + const ScrDyn *from = found->view; + bool keep = false; + size_t next = 0; + for (size_t i = 0; i < from->v.obj.len && !keep; i++) { + const ScrDynEntry *entry = &from->v.obj.entries[i]; + size_t index = scr_record_source_index(&probe, entry->key, entry->key_len); + if (index == count) continue; + keep = index != next; + next++; + } + if (!keep) return; + ScrDyn *view = scr_dyn_new_obj(); + for (size_t i = 0; i < from->v.obj.len; i++) { + const ScrDynEntry *entry = &from->v.obj.entries[i]; + if (scr_record_source_declared(&probe, entry->key, entry->key_len)) + scr_dyn_obj_set(view, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined())); + } + ScrRecordSource *old = scr_record_source_find(dst); + if (old) scr_record_source_drop(old); + probe.view = view; + scr_record_source_put(&probe); +} + void scr_record_source_move(void *from, void *to) { if (!scr_record_source_count) return; ScrRecordSource *old = scr_record_source_find(to); diff --git a/tests/corpus/7750-key-order-javascript-spread-sources/lib.js b/tests/corpus/7750-key-order-javascript-spread-sources/lib.js new file mode 100644 index 000000000..c8fc0d066 --- /dev/null +++ b/tests/corpus/7750-key-order-javascript-spread-sources/lib.js @@ -0,0 +1,14 @@ +// Records JavaScript builds in its own key order, handed to TypeScript +// through typed slots: a draft from a "validator", a stored row, and a +// replacement method installed through a computed key. +export function validate(input) { + return { year: input.year, title: input.title, tags: input.tags }; +} + +export function storeRow(rows, id) { + rows.set(id, { title: "Stored", id, tags: [], year: 1999, createdAt: "t0", version: 1 }); +} + +export function replace(target, name) { + target[name] = (id) => ({ version: 7, id, year: 2001, title: "Replaced", tags: ["r"], createdAt: "t1" }); +} diff --git a/tests/corpus/7750-key-order-javascript-spread-sources/main.ts b/tests/corpus/7750-key-order-javascript-spread-sources/main.ts new file mode 100644 index 000000000..727f8e367 --- /dev/null +++ b/tests/corpus/7750-key-order-javascript-spread-sources/main.ts @@ -0,0 +1,105 @@ +// Literals that start by spreading a record JavaScript built keep that +// record's key order, as Node does: a draft from JavaScript spread into a +// new record, a self copy of a record JavaScript stored or returned, and +// a wider view of either. Native drafts and copies keep the literal order. +import { replace, storeRow, validate } from "./lib.js"; + +interface Book { + id: string; + title: string; + year: number; + tags: readonly string[]; + createdAt: string; + version: number; +} + +type Draft = Omit; +type Patch = Partial; + +interface Availability { + copies: number; + onLoan: number; +} + +type BookView = Book & { availability: Availability }; + +class Repository { + private readonly rows = new Map(); + private sequence = 0; + + create(draft: Draft): Book { + const book: Book = { + ...draft, + tags: [...draft.tags].sort(), + id: `bk_${++this.sequence}`, + createdAt: "t" + this.sequence, + version: 1, + }; + this.rows.set(book.id, book); + return book; + } + + require(id: string): Book { + const row = this.rows.get(id); + if (row === undefined) throw new Error("missing " + id); + return row; + } + + update(id: string, patch: Patch): Book { + const current = this.require(id); + const next: Book = { ...current, ...patch, version: current.version + 1 }; + this.rows.set(id, next); + return next; + } + + view(book: Book): BookView { + return { ...book, availability: { copies: 2, onLoan: book.version } }; + } + + table(): Map { + return this.rows; + } +} + +function show(label: string, value: Book): void { + console.log(label, JSON.stringify(value)); + console.log(" keys", Object.keys(value).join()); +} + +function showView(label: string, value: BookView): void { + console.log(label, JSON.stringify(value)); + console.log(" keys", Object.keys(value).join()); +} + +const repo = new Repository(); + +// A native draft: the literal's order. +const native = repo.create({ title: "Native", year: 1990, tags: ["b", "a"] }); +show("native", native); +showView("native view", repo.view(native)); +show("native update", repo.update(native.id, { year: 1991 })); + +// A draft JavaScript built in another order. +const draft: Draft = validate({ title: "Checked", year: 2020, tags: ["z", "y"] }); +const checked = repo.create(draft); +show("checked", checked); +showView("checked view", repo.view(checked)); +show("checked update", repo.update(checked.id, { title: "Checked 2" })); +showView("checked update view", repo.view(repo.require(checked.id))); + +// A row JavaScript stored into the typed map. +storeRow(repo.table(), "js_1"); +show("stored", repo.require("js_1")); +show("stored update", repo.update("js_1", { tags: ["s"] })); +showView("stored view", repo.view(repo.require("js_1"))); + +// A replacement method returning rows JavaScript builds. +replace(repo, "require"); +show("replaced", repo.require("x")); +show("replaced update", repo.update("x", { year: 2002 })); +showView("replaced view", repo.view(repo.require("y"))); + +// The copies are independent records. +const again = repo.view(checked); +showView("again", again); +console.log(native.version, checked.version, again.availability.onLoan); diff --git a/tests/harness/corpus-costs/7750-key-order-javascript-spread-sources/main.ts.json b/tests/harness/corpus-costs/7750-key-order-javascript-spread-sources/main.ts.json new file mode 100644 index 000000000..eb6dbd224 --- /dev/null +++ b/tests/harness/corpus-costs/7750-key-order-javascript-spread-sources/main.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 9200 } From f14997776b804884e9d71be36c690c772e44ab93 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 09:52:30 -0700 Subject: [PATCH 19/45] Box TextEncoder and TextDecoder records as checked-dynamic handles A codec stored in untyped storage (hono's StreamingApi keeps `this.encoder = new TextEncoder()` in a JavaScript class field) had no dynamic representation and threw SC1101 at run time. The codec records now cross 'unknown' slots as handles of two new tags that hold the record itself, so identity and a decoder's streaming state survive the round trip, and checked casts unbox the same record. The handles answer encode, encodeInto and decode (with options objects), the encoding, fatal and ignoreBOM properties, method values with Node's ERR_INVALID_THIS, instanceof, String() tags and JSON's {}. The unit scr_text_codec_dyn.c links only into programs that convert a codec to or from an untyped value (the textCodecDyn runtime feature, found with the dgram carrier walk, now shared). Co-Authored-By: Claude Opus 5.5 --- .../builtin-apis/text-encoding.mdx | 1 + .../generated/node-v24-internal.json | 20 +- internal/compatibility/static-support.json | 4 +- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../src/backend/llvm/dyn-handle-tags.test.ts | 8 +- packages/compiler/src/backend/llvm/dyn.ts | 122 ++++++++++ .../compiler/src/backend/llvm/expr-dynamic.ts | 6 +- .../src/backend/llvm/expr-stream-bridges.ts | 14 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 1 + .../src/backend/runtime-pack-targets.test.ts | 1 + .../compiler/src/backend/runtime-pack.test.ts | 1 + packages/compiler/src/backend/warm-cache.ts | 1 + .../src/executable/executable-cache.test.ts | 1 + .../src/executable/executable-cache.ts | 2 + .../lowering/expressions/relational.ts | 16 ++ packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/analysis.ts | 7 +- .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/ir.ts | 16 ++ packages/compiler/src/ir/runtime-features.ts | 110 +++++++-- packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + .../7747-text-codec-untyped-slot.js.json | 6 + .../7749-text-codec-unknown-roundtrip.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_bytes.h | 12 + packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_text_codec_dyn.c | 215 ++++++++++++++++++ tests/corpus/7747-text-codec-untyped-slot.js | 69 ++++++ .../7749-text-codec-unknown-roundtrip.ts | 22 ++ .../7747-text-codec-untyped-slot.js.json | 1 + .../7749-text-codec-unknown-roundtrip.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + 39 files changed, 650 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json create mode 100644 packages/runtime/src/scr_text_codec_dyn.c create mode 100644 tests/corpus/7747-text-codec-untyped-slot.js create mode 100644 tests/corpus/7749-text-codec-unknown-roundtrip.ts create mode 100644 tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json create mode 100644 tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json diff --git a/docs/content/limitations/builtin-apis/text-encoding.mdx b/docs/content/limitations/builtin-apis/text-encoding.mdx index bffd24424..e20cfd943 100644 --- a/docs/content/limitations/builtin-apis/text-encoding.mdx +++ b/docs/content/limitations/builtin-apis/text-encoding.mdx @@ -1,6 +1,7 @@ - **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures. Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view. Codec properties expose the canonical encoding and decoder flags. + Codecs held in `any` or `unknown` storage keep their identity and streaming state, and dynamic code can call `encode`, `encodeInto` and `decode`, with options objects, and read their properties. Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings. Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings. Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index ba8692b49..f2b528d7a 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -52194,6 +52194,7 @@ "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/6331-jose-root-entry/main.ts", + "tests/corpus/7747-text-codec-untyped-slot.js", "tests/corpus/text-codec-values/main.ts", "tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-dylink/main.mjs", @@ -53346,7 +53347,8 @@ "tests": [ "tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -53397,7 +53399,8 @@ "tests": [ "tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -101277,6 +101280,7 @@ "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/6428-promisify-callback-functions.ts", + "tests/corpus/7747-text-codec-untyped-slot.js", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -101805,7 +101809,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -101926,7 +101931,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102036,7 +102042,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102065,7 +102072,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 100882c1a..5950be0a5 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -332,11 +332,11 @@ { "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] }, { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, - { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] }, { "chapter": "globals", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5287-blob-inspect.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js"] }, { "chapter": "buffer", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5288-blob-validation.js", "tests/fixtures/node-types/blob-file.ts"] }, { "chapter": "buffer", "signatures": ["`blob.arrayBuffer()`", "`blob.bytes()`", "`blob.text()`", "`blob.slice([start[, end[, type]]])`", "`blob.size`", "`type` Type: {string}", "`name` Type: {string}", "`lastModified` Type: {number}"], "status": "supported", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5289-blob-read-order.ts", "tests/corpus/5290-blob-dynamic.js", "tests/fixtures/node-types/blob-file.ts"] }, - { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] }, { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] }, { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] }, { "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] }, diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 32318d53e..a9076af22 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -50,6 +50,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dgram: features.dgram, dnsPromises: features.dnsPromises, dgramDyn: features.dgramDyn, + textCodecDyn: features.textCodecDyn, watch: features.fsWatch, foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []), nodeTest: features.nodeTest, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index e9f52f7a0..79be316db 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -54,6 +54,7 @@ export async function compilePackedLibrary( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts index c015b85d0..8c3e5de80 100644 --- a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts +++ b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { expect, test } from "vitest"; import { DYN_HANDLE_KINDS } from "../../ir/ir.js"; -import { DYN_HANDLE_TAG_NUM } from "./dyn.js"; +import { DYN_HANDLE_TAG_NUM, TEXT_CODEC_TAG_NUM } from "./dyn.js"; // SCR_DYNH_TAGS in scr_dyn.h lists the ScrDynHandleTag values by name, each // with an explicit value, so changes that add tags edit different lines. The @@ -39,3 +39,9 @@ test("the backend's handle tag numbers match scr_dyn.h", () => { expect(values.get(tag!), `${kind} (${tag})`).toBe(num); } }); + +test("the backend's codec handle tag numbers match scr_dyn.h", () => { + const values = new Map(tags.map((tag) => [tag.name, tag.value])); + expect(values.get("SCR_DYNH_TEXT_DECODER")).toBe(TEXT_CODEC_TAG_NUM.TextDecoder); + expect(values.get("SCR_DYNH_TEXT_ENCODER")).toBe(TEXT_CODEC_TAG_NUM.TextEncoder); +}); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86626d304..3987c52c2 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -43,6 +43,8 @@ import { isDynTypedRefType, isRefCounted, isUnitType, + recordTextCodecClass, + textCodecRecordClass, typeKey, } from "../../ir/ir.js"; import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js"; @@ -94,6 +96,17 @@ export const DYN_HANDLE_TAG_NUM: Record = { cryptoHmac: 53, }; +/** The handle tags of TextEncoder and TextDecoder records boxed as dyn + * (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */ +export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = { + TextDecoder: 76, + TextEncoder: 77, +}; + +/** The decoder record fields scr_text_decoder_to_dyn reads, in its + * parameter order. */ +const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const; + /** Handle kinds whose runtime unit boxes them through its own function, * which installs the tag's ops on first use (scr_crypto.h). Programs that * never box one of these values do not link its dynamic members. */ @@ -927,6 +940,21 @@ export class LlDyn { const name = `sc_dm_${this.dynMatchers.size}`; this.dynMatchers.set(matcherKey, name); this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]); + const codec = textCodecRecordClass(t, (id) => this.host.recordsById.get(id)); + if (codec !== null) { + // A codec matches only its own handle box. + const B = new BlockBuilder(); + const matched = this.handleTagMatches(B, TEXT_CODEC_TAG_NUM[codec]); + B.terminate(`ret i1 ${matched}`); + this.defs.push( + `define internal zeroext i1 @${name}(ptr %d) ${FN_ATTRS} { ; matches ${key}`, + B.render(), + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } const recursive = this.isRecursiveShape(t); const wrapped = recursive || this.holdsRecursiveShape(t); const B = new BlockBuilder(); @@ -1643,6 +1671,24 @@ export class LlDyn { this.host.journalBuilt?.(); return name; } + const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id)); + if (codec !== null) { + // The record the codec's handle box holds (+1), or the path TypeError. + host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`); + const checked = B.tmp(); + B.line( + `${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`, + ); + B.terminate(`ret ptr ${checked}`); + this.defs.push( + `define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`, + B.render(), + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } if (isRefCounted(t) && t.kind !== "dyn") { host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`); if (t.kind === "object" && t.className === "%Error") { @@ -2671,6 +2717,77 @@ export class LlDyn { return name; } + /** `i1`: %d is a handle box with tag `tag`. */ + private handleTagMatches(B: BlockBuilder, tag: number): string { + const kd = this.kindOf(B, "%d"); + const isHandle = B.tmp(); + B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`); + const lHandle = B.newLabel("dm.handle"); + const lDone = B.newLabel("dm.handle.done"); + const entry = B.currentLabel(); + B.condBr(isHandle, lHandle, lDone); + B.startBlock(lHandle); + const tagPtr = B.tmp(); + const loaded = B.tmp(); + B.line( + `${tagPtr} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(24, 20)} ; ->v.handle.tag`, + ); + B.line(`${loaded} = load i32, ptr ${tagPtr}`); + const same = B.tmp(); + B.line(`${same} = icmp eq i32 ${loaded}, ${tag}`); + B.br(lDone); + B.startBlock(lDone); + const matched = B.tmp(); + B.line(`${matched} = phi i1 [ false, %${entry} ], [ ${same}, %${lHandle} ]`); + return matched; + } + + /** A TextEncoder or TextDecoder record boxes as its handle (+1), which + * holds the record by reference. A decoder passes the byte offsets of the + * fields its boxed decode reads. */ + private textCodecToDyn( + B: BlockBuilder, + t: IrType & { kind: "record" }, + shape: IrRecordShape, + codec: "TextEncoder" | "TextDecoder", + ): void { + const host = this.host; + const rc = vAdapters(host, t); + const r = B.tmp(); + if (codec === "TextEncoder") { + host.declare(`declare ptr @scr_text_encoder_to_dyn(ptr, ptr, ptr)`); + B.line( + `${r} = call ptr @scr_text_encoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release})`, + ); + } else { + const struct = mangleRecordStruct(t.shapeId); + const base = host.nullableUnions.fieldBase(t.shapeId); + const start = B.tmp(); + B.line(`${start} = ptrtoint ptr %v to ${this.S}`); + const offsets = TEXT_DECODER_FIELDS.map((field) => { + const index = shape.fields.findIndex((f) => f.name === field); + if (index < 0) + throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`); + const p = B.tmp(); + const at = B.tmp(); + const offset = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${index + base} ; .${llvmCommentText(field)}`, + ); + B.line(`${at} = ptrtoint ptr ${p} to ${this.S}`); + B.line(`${offset} = sub ${this.S} ${at}, ${start}`); + return `${this.S} ${offset}`; + }); + host.declare( + `declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`, + ); + B.line( + `${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`, + ); + } + B.terminate(`ret ptr ${r}`); + } + /* ── toDynHelper (walkers.ts, ported) ─────────────────────────── */ /** `sc_td_( v) -> ptr` — build a fresh dyn value from a @@ -2846,6 +2963,11 @@ export class LlDyn { const shape = host.recordsById.get(t.shapeId); if (!shape) throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`); + const codec = recordTextCodecClass(shape); + if (codec !== null) { + this.textCodecToDyn(B, t, shape, codec); + break; + } const struct = mangleRecordStruct(t.shapeId); const base = host.nullableUnions.fieldBase(t.shapeId); const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base])); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 07a538184..26905a7ef 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -18,6 +18,7 @@ import { type IrExpr, type IrType, isDynTypedRefType, + textCodecRecordClass, isRefCounted, isUnitType, typeEquals, @@ -86,7 +87,10 @@ export function emitDynamicExpr( // Bytes already box their shared mutable storage directly. A second // capsule would split identity between checked and native views. const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className); - if (!errorRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { + // A TextEncoder or TextDecoder record boxes as its own handle, which + // keeps the record's identity (toDynHelper). + const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null; + if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { if (v.type.kind === "union") { const adapter = host.liveDynUnionRefAdapter(v.type); const boxed = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index aee7be180..a2f7208ec 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -16,6 +16,7 @@ import { KEY_ORDER_FIELD, shapeKeyOrders, isDynTypedRefType, + textCodecRecordClass, isRefCounted, shapeHasAccessorSlots, typeKey, @@ -597,12 +598,21 @@ export function liveDynUnionRefAdapter( if (!union) { throw new InternalCompilerError(`llvm emitter bug: live dyn ref of unknown union ${t.unionId}`); } + // Codec records box as their own handles, by reference, through toDynHelper. + const codecArm = (arm: IrType): boolean => + textCodecRecordClass(arm, (id) => host.recordsById.get(id)) !== null; const mutableArms = union.arms .map((arm, tag) => ({ arm, tag })) .filter( - ({ arm }) => arm.kind !== "bytes" && (streamTypedRefEligible(arm) || isDynTypedRefType(arm)), + ({ arm }) => + arm.kind !== "bytes" && + !codecArm(arm) && + (streamTypedRefEligible(arm) || isDynTypedRefType(arm)), ); - if (mutableArms.length === 0 && !union.arms.some((arm) => arm.kind === "bytes")) { + if ( + mutableArms.length === 0 && + !union.arms.some((arm) => arm.kind === "bytes" || codecArm(arm)) + ) { throw new InternalCompilerError(`llvm emitter bug: live dyn ref of immutable union ${key}`); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index af32c6936..7b411f2bb 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -560,6 +560,7 @@ export const LIB_FN_SYMS: Record = { "text.decoderEncoding": "scr_text_decoder_encoding", "text.decoderName": "scr_text_decoder_name", "text.encodeInto": "scr_text_encode_into", + "text.codecIs": "scr_text_codec_is", "buffer.isAscii": "scr_buffer_is_ascii", "buffer.isUtf8": "scr_buffer_is_utf8", "buffer.transcode": "scr_buffer_transcode", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index e018037fd..c073bb9a6 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -81,6 +81,9 @@ export interface NativeLinkFeatures { dnsPromises: boolean; /** dgram sockets as untyped values: scr_dgram_dyn.c. */ dgramDyn: boolean; + /** TextEncoder and TextDecoder records as untyped values: + * scr_text_codec_dyn.c. */ + textCodecDyn: boolean; watch: boolean; foreignFfi: boolean; nodeTest: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 61de82e72..53e160984 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -220,6 +220,10 @@ export interface CcOptions { * scr_dgram_dyn.c over the dgram unit, so typed dgram binaries keep * their exact link line. */ dgramDyn?: boolean; + /** A TextEncoder or TextDecoder record converts to or from an untyped + * value: compiles scr_text_codec_dyn.c, so typed codec binaries keep + * their exact link line. */ + textCodecDyn?: boolean; /** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles * scr_watch.c into the binary — the net gating precedent, so watch-free diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 233b9cac2..1578a0c80 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -302,6 +302,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dgram, "scr_dgram.c"), unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"), unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"), + unit((u) => u.opts.textCodecDyn, "scr_text_codec_dyn.c"), unit((u) => u.opts.watch, "scr_watch.c"), unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 44c3d6290..9846e8d89 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -118,6 +118,7 @@ const features = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index e3bc999a7..e2b24f8ca 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -54,6 +54,7 @@ const BASE: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index b48cf6739..02acb5462 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 5cf8c6588..e066c0412 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -58,6 +58,7 @@ const native: EarlyExecutableNativeFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 7b7febc06..96922ca13 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -92,6 +92,7 @@ export interface EarlyExecutableNativeFeatures { dgram: boolean; dnsPromises: boolean; dgramDyn: boolean; + textCodecDyn: boolean; watch: boolean; foreignFfi: boolean; nodeTest: boolean; @@ -235,6 +236,7 @@ const BOOLEAN_NATIVE_KEYS = [ "dgram", "dnsPromises", "dgramDyn", + "textCodecDyn", "watch", "foreignFfi", "nodeTest", diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index f1590e9cc..1651bf0d9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -338,6 +338,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc type: BOOL, loc, }; + // A codec in untyped storage is its record's handle box. + const codec = (["TextEncoder", "TextDecoder"] as const).find((name) => + lowerer.isStdlibGlobal(expr.right, name), + ); + if (codec && lowerer.mapTypeOf(lowerer.typeOf(expr.left))?.kind === "dyn") { + return { + kind: "libCall", + fn: "text.codecIs", + args: [ + lowerer.lowerExprExpecting(expr.left, DYN), + { kind: "boolLit", value: codec === "TextDecoder", type: BOOL, loc }, + ], + type: BOOL, + loc, + }; + } if (lowerer.isStdlibGlobal(expr.right, "ReadableStream")) { const value = lowerer.lowerExprExpecting(expr.left, DYN); return { kind: "libCall", fn: "fetch.streamIs", args: [value], type: BOOL, loc }; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 4f7b93cfd..ae93598d6 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -623,6 +623,7 @@ async function compileExecutableNative( dgram: features.dgram, dnsPromises: features.dnsPromises, dgramDyn: features.dgramDyn, + textCodecDyn: features.textCodecDyn, watch: features.watch, foreignFfi: features.foreignFfi, ...(features.workers ? { workers: true } : {}), diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 70ec381a1..ee1cee6be 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -4,6 +4,7 @@ import { BYTES_ELEMENT_NAME, DYN_HANDLE_KINDS, RUNTIME_STREAM_CLASSES, + recordTextCodecClass, typeEquals, type IrExpr, type IrLibFn, @@ -141,8 +142,10 @@ export function dynDesc( return "string"; case "bool": return "boolean"; - case "record": - return recordsById.get(t.shapeId)?.tuple ? "array" : "object"; + case "record": { + const shape = recordsById.get(t.shapeId); + return shape?.tuple ? "array" : ((shape && recordTextCodecClass(shape)) ?? "object"); + } case "array": return "array"; case "nullT": diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1be0f95cd..9f5b3334f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4852,6 +4852,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "text.decoderEncoding": { argTypes: [DYN], result: F64 }, "text.decoderName": { argTypes: [F64], result: STRING }, "text.encodeInto": { argTypes: [DYN, DYN], result: DYN }, + /** `value instanceof TextDecoder` (true) or `TextEncoder` (false) on a + * checked-dynamic value: a codec record's handle box (scr_text_codec_dyn.c). + * Borrowed; never throws. */ + "text.codecIs": { argTypes: [DYN, BOOL], result: BOOL }, "buffer.isAscii": { argTypes: [DYN], result: BOOL }, "buffer.isUtf8": { argTypes: [DYN], result: BOOL }, "buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 878088d65..29dda102d 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1745,6 +1745,18 @@ export function recordTextCodecClass(shape: IrRecordShape): "TextEncoder" | "Tex return null; } +/** The codec class of a TextEncoder or TextDecoder record type, else null. + * Such a record crosses an `unknown` slot as a native handle that holds the + * record itself (scr_text_codec_dyn.c), never as a copy or a field view. */ +export function textCodecRecordClass( + t: IrType, + getRecord: (shapeId: string) => IrRecordShape | undefined, +): "TextEncoder" | "TextDecoder" | null { + if (t.kind !== "record") return null; + const shape = getRecord(t.shapeId); + return shape ? recordTextCodecClass(shape) : null; +} + /** Object-literal ACCESSOR properties (`{ get x() {...}, set x(v) {...} }`) * live on the shape as reserved '%'-fields holding closures: `%get:x` a * `() => T` invoked once per property READ (side effects and all — JS's @@ -4273,6 +4285,7 @@ export function canConvertToDyn( visiting: Set = new Set(), ): boolean { if (isJsonSafeType(t, getRecord, getUnion)) return true; + if (textCodecRecordClass(t, getRecord) !== null) return true; // numeric typed arrays and boxable functions are dyn kinds the walker boxes // ANYWHERE (bytes and functions held by identity), including nested // in records/arrays/unions. isJsonSafeType rejects them, but dynFrom @@ -4319,6 +4332,7 @@ export function canConvertToDyn( // either its result or a promise of it. canBoxDynComposite(a, getRecord, getUnion, visiting) || isDynTypedRefType(a) || + textCodecRecordClass(a, getRecord) !== null || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) || (a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion, visiting)) || @@ -4499,6 +4513,8 @@ function canDynCheckToAt( visiting.delete(key); return result; } + // A codec handle unboxes to the record it holds. + if (textCodecRecordClass(t, getRecord) !== null) return true; if (t.kind === "record") { const key = typeKey(t); if (visiting.has(key)) return false; diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4ad3dcc5c..e1e7e28be 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -2,6 +2,7 @@ import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } import type { IrLibFn } from "./builtin-signatures.js"; import { isDirentShape, + recordTextCodecClass, RUNTIME_EMITTER_CLASS, RUNTIME_EVENT_CLASS, RUNTIME_EVENT_TARGET_CLASS, @@ -415,7 +416,10 @@ interface TypeTables { unions: Map; classes: Map; /** mentionsDgramSocket's carriers, computed on first use. */ - dgramCarriers?: DgramCarriers; + dgramCarriers?: TypeCarriers; + /** mentionsTextCodec's and mentionsTextDecoder's carriers. */ + textCodecCarriers?: TypeCarriers; + textDecoderCarriers?: TypeCarriers; } /** Whether boxing a value of type `t` as dyn boxes a promise, which goes @@ -479,30 +483,42 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean } } -/** The record shapes, unions and classes whose values carry a dgram socket - * (directly or through other members), computed once per module. */ -interface DgramCarriers { +/** The record shapes, unions and classes whose values carry a leaf type + * (directly or through other members), computed once per module and leaf. */ +interface TypeCarriers { records: Set; unions: Set; anyClass: boolean; } -/** Whether `t` carries a dgram socket, given the carrier sets so far: - * structural through functions (parameters and result), promises and - * collections; named records, unions and class instances by the sets. */ -function carriesDgram(t: IrType, carriers: DgramCarriers): boolean { +/** The leaf a carrier set tracks: a dgram socket, a codec record. */ +type CarrierLeaf = (t: IrType, tables: TypeTables) => boolean; + +/** Whether `t` carries a leaf, given the carrier sets so far: structural + * through functions (parameters and result), promises and collections; + * named records, unions and class instances by the sets. */ +function carriesLeaf( + t: IrType, + carriers: TypeCarriers, + leaf: CarrierLeaf, + tables: TypeTables, +): boolean { + if (leaf(t, tables)) return true; switch (t.kind) { - case "dgramSocket": - return true; case "func": - return t.params.some((p) => carriesDgram(p, carriers)) || carriesDgram(t.ret, carriers); + return ( + t.params.some((p) => carriesLeaf(p, carriers, leaf, tables)) || + carriesLeaf(t.ret, carriers, leaf, tables) + ); case "promise": - return carriesDgram(t.inner, carriers); + return carriesLeaf(t.inner, carriers, leaf, tables); case "array": case "set": - return carriesDgram(t.elem, carriers); + return carriesLeaf(t.elem, carriers, leaf, tables); case "map": - return carriesDgram(t.key, carriers) || carriesDgram(t.value, carriers); + return ( + carriesLeaf(t.key, carriers, leaf, tables) || carriesLeaf(t.value, carriers, leaf, tables) + ); case "record": return carriers.records.has(t.shapeId); case "union": @@ -515,25 +531,29 @@ function carriesDgram(t: IrType, carriers: DgramCarriers): boolean { } } -/** The module's dgram carriers: a fixpoint over its records, unions and +/** The module's carriers of `leaf`: a fixpoint over its records, unions and * class fields. */ -function dgramCarriers(tables: TypeTables): DgramCarriers { - const carriers: DgramCarriers = { records: new Set(), unions: new Set(), anyClass: false }; +function leafCarriers(tables: TypeTables, leaf: CarrierLeaf): TypeCarriers { + const carriers: TypeCarriers = { records: new Set(), unions: new Set(), anyClass: false }; + const carries = (t: IrType): boolean => carriesLeaf(t, carriers, leaf, tables); for (let changed = true; changed;) { changed = false; for (const [id, shape] of tables.records) - if (!carriers.records.has(id) && shape.fields.some((f) => carriesDgram(f.type, carriers))) { + if ( + !carriers.records.has(id) && + (leaf({ kind: "record", shapeId: id }, tables) || shape.fields.some((f) => carries(f.type))) + ) { carriers.records.add(id); changed = true; } for (const [id, def] of tables.unions) - if (!carriers.unions.has(id) && def.arms.some((a) => carriesDgram(a, carriers))) { + if (!carriers.unions.has(id) && def.arms.some(carries)) { carriers.unions.add(id); changed = true; } if ( !carriers.anyClass && - [...tables.classes.values()].some((c) => c.fields.some((f) => carriesDgram(f.type, carriers))) + [...tables.classes.values()].some((c) => c.fields.some((f) => carries(f.type))) ) { carriers.anyClass = true; changed = true; @@ -542,13 +562,42 @@ function dgramCarriers(tables: TypeTables): DgramCarriers { return carriers; } +const dgramLeaf: CarrierLeaf = (t) => t.kind === "dgramSocket"; + /** Whether converting a value of type `t` to or from dyn converts a dgram * socket, which goes through scr_dgram_dyn.c: the socket itself, or a * function, collection, record, union, promise or class instance that * carries one (boxed functions convert their parameters too). */ function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean { - tables.dgramCarriers ??= dgramCarriers(tables); - return carriesDgram(t, tables.dgramCarriers); + tables.dgramCarriers ??= leafCarriers(tables, dgramLeaf); + return carriesLeaf(t, tables.dgramCarriers, dgramLeaf, tables); +} + +const textCodecLeaf: CarrierLeaf = (t, tables) => { + if (t.kind !== "record") return false; + const shape = tables.records.get(t.shapeId); + return shape !== undefined && recordTextCodecClass(shape) !== null; +}; + +const textDecoderLeaf: CarrierLeaf = (t, tables) => { + if (t.kind !== "record") return false; + const shape = tables.records.get(t.shapeId); + return shape !== undefined && recordTextCodecClass(shape) === "TextDecoder"; +}; + +/** Whether converting a value of type `t` to or from dyn converts a + * TextEncoder or TextDecoder record, which boxes it as a handle of + * scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */ +function mentionsTextCodec(t: IrType, tables: TypeTables): boolean { + tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf); + return carriesLeaf(t, tables.textCodecCarriers, textCodecLeaf, tables); +} + +/** The TextDecoder half: its boxed decode runs scr_text_decode_stream, + * which reaches the legacy decoders. */ +function mentionsTextDecoder(t: IrType, tables: TypeTables): boolean { + tables.textDecoderCarriers ??= leafCarriers(tables, textDecoderLeaf); + return carriesLeaf(t, tables.textDecoderCarriers, textDecoderLeaf, tables); } /** The libCall fns served by the OPTIONAL events unit (scr_events.c): @@ -794,6 +843,15 @@ const RUNTIME_FEATURE_RULES = { (node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) || (node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())), }), + /** A TextEncoder or TextDecoder record converts to or from an untyped + * value (its handle box, or a boxed function, record or collection that + * carries one): scr_text_codec_dyn.c. */ + textCodecDyn: rule({ + fns: ["text.codecIs"], + expr: (node, tables) => + (node.kind === "dynFrom" && mentionsTextCodec(node.value.type, tables())) || + (node.kind === "dynCheck" && mentionsTextCodec(node.type, tables())), + }), /** node:dns/promises: scr_dns_promises.c over the dgram unit. */ dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }), dynAsync: rule({ @@ -942,7 +1000,12 @@ const RUNTIME_FEATURE_RULES = { prefixes: ["insp.", "console.", "test."], libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!), }), - legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }), + legacyTextDecoder: rule({ + fns: [...LEGACY_TEXT_DECODER_LIB_FNS], + expr: (node, tables) => + (node.kind === "dynFrom" && mentionsTextDecoder(node.value.type, tables())) || + (node.kind === "dynCheck" && mentionsTextDecoder(node.type, tables())), + }), /** The program calls mock.timers members: its timer calls go through * scr_test.c's mock-aware entry points. */ mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }), @@ -1056,6 +1119,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dc: false, dgram: false, dgramDyn: false, + textCodecDyn: false, dnsPromises: false, dynAsync: false, dynBuiltinClass: false, diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 21047b929..a35ce811f 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -122,6 +122,7 @@ async function main(): Promise { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index f7f399f80..a238029dc 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -239,6 +239,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "dgram", "dnsPromises", "dgramDyn", + "textCodecDyn", "watch", "foreignFfi", "nodeTest", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 4c031a8e7..33a4c19b9 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -114,6 +114,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json new file mode 100644 index 000000000..23efb7db6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7747-text-codec-untyped-slot.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json new file mode 100644 index 000000000..dd9f068f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7749-text-codec-unknown-roundtrip.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 89b6eaf19..0ab467236 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -148,6 +148,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_dns_promises.c", all("dgram", "dnsPromises")], // Sockets as untyped values run over the dgram unit. ["scr_dgram_dyn.c", all("dgram", "dgramDyn")], + // TextEncoder and TextDecoder records boxed as untyped values. + ["scr_text_codec_dyn.c", "textCodecDyn"], ["scr_watch.c", "watch"], ["scr_ffi_queue.c", "foreignFfi"], ["scr_test.c", "nodeTest"], diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9b382dda5..2e4b44409 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -357,6 +357,18 @@ ScrStr *scr_text_decode(const ScrBytes *b); double scr_text_decoder_encoding(const ScrDyn *label); ScrStr *scr_text_decoder_name(double encoding); ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination); +/* TextEncoder and TextDecoder records as checked-dynamic handles + * (scr_text_codec_dyn.c, linked by the textCodecDyn runtime feature). The + * boxes borrow the record and retain it through `retain`; a decoder also + * names the byte offsets of its encoding (f64), fatal and ignoreBOM (i8) + * and state (dyn) fields. from_dyn answers the retained record or throws + * the path-annotated TypeError and answers NULL. */ +ScrDyn *scr_text_encoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *)); +ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t fatal, size_t ignore_bom, size_t state); +void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const struct ScrDynPath *path, + const char *want); +bool scr_text_codec_is(const ScrDyn *d, bool decoder); bool scr_buffer_is_ascii(const ScrDyn *input); bool scr_buffer_is_utf8(const ScrDyn *input); ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 6f0c28371..740e33a09 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -200,6 +200,8 @@ typedef enum { X(SHARED_ARRAY_BUFFER, 37) \ X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \ + X(TEXT_DECODER, 76) /* compiled TextDecoder record (scr_text_codec_dyn.c) */ \ + X(TEXT_ENCODER, 77) /* compiled TextEncoder record (scr_text_codec_dyn.c) */ \ X(URL, 28) /* native WHATWG URL */ \ X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ X(WASM_GLOBAL, 61) /* WebAssembly.Global */ \ diff --git a/packages/runtime/src/scr_text_codec_dyn.c b/packages/runtime/src/scr_text_codec_dyn.c new file mode 100644 index 000000000..5102bc558 --- /dev/null +++ b/packages/runtime/src/scr_text_codec_dyn.c @@ -0,0 +1,215 @@ +/* WHATWG TextEncoder and TextDecoder instances as checked-dynamic values + * (SCR_DYNH_TEXT_ENCODER, SCR_DYNH_TEXT_DECODER). A codec is a compiled + * record (type-mapper.ts): the handle holds that record by reference, so + * a codec stored in an untyped slot and read back keeps its identity and + * a decoder keeps its streaming state. The compiler passes the record's + * reference-count entry points and, for decoders, the byte offsets of the + * encoding, fatal, ignoreBOM and state fields when it boxes one. A unit + * of its own so programs that never box a codec link none of it + * (native-toolchain gating: the textCodecDyn runtime feature). */ +#include "scr_platform.h" +#include "scr_runtime.h" + +#include + +typedef struct { + void *(*retain)(void *); + void (*release)(void *); + size_t encoding, fatal, ignore_bom, state; +} ScrTextCodecLayout; + +/* A program has one record shape per codec class, so one layout each. */ +static SCR_TL ScrTextCodecLayout scr_tcd_encoder_layout, scr_tcd_decoder_layout; + +static void *scr_tcd_encoder_retain(void *rec) { return scr_tcd_encoder_layout.retain(rec); } +static void scr_tcd_encoder_release(void *rec) { scr_tcd_encoder_layout.release(rec); } +static void *scr_tcd_decoder_retain(void *rec) { return scr_tcd_decoder_layout.retain(rec); } +static void scr_tcd_decoder_release(void *rec) { scr_tcd_decoder_layout.release(rec); } + +static void scr_tcd_codec_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + +/* Boxes an owned string (+1 box; the string's reference moves in). */ +static ScrDyn *scr_tcd_str(ScrStr *text) { + ScrDyn *boxed = scr_dyn_new_str(text); + scr_str_release(text); + return boxed; +} + +static ScrDyn *scr_tcd_codec_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +static double scr_tcd_decoder_encoding(const void *rec) { + double encoding; + memcpy(&encoding, (const char *)rec + scr_tcd_decoder_layout.encoding, sizeof encoding); + return encoding; +} + +static bool scr_tcd_decoder_flag(const void *rec, size_t offset) { + return ((const unsigned char *)rec)[offset] != 0; +} + +static ScrDyn *scr_tcd_encoder_invoke(void *rec, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)rec; + (void)self; + if (strcmp(method, "encode") == 0) { + /* encode(input = ""): any other input converts with ToString. */ + ScrDyn *input = scr_tcd_codec_arg(args, argc, 0); + ScrStr *text = + input->kind == SCR_DYN_UNDEF ? scr_str_new("", 0) : scr_dyn_string_coerce_js(input); + if (!text) return NULL; + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_bytes_from_str(text, utf8); + scr_str_release(utf8); + scr_str_release(text); + /* A fresh Uint8Array, not a Buffer. */ + bytes->is_buffer = false; + ScrDyn *result = scr_dyn_new_bytes(bytes); + scr_bytes_release(bytes); + return result; + } + if (strcmp(method, "encodeInto") == 0) + return scr_text_encode_into(scr_tcd_codec_arg(args, argc, 0), + scr_tcd_codec_arg(args, argc, 1)); + scr_tcd_codec_not_a_function(what); + return NULL; +} + +static ScrDyn *scr_tcd_encoder_get(void *rec, const char *key, size_t len) { + (void)rec; + if (len == 8 && memcmp(key, "encoding", 8) == 0) return scr_tcd_str(scr_str_new("utf-8", 5)); + return NULL; +} + +/* decode(input, options): the options object's `stream` member, read + * with JS truthiness; undefined and null mean no options. */ +static bool scr_tcd_decoder_stream_option(const ScrDyn *options, bool *stream) { + *stream = false; + if (options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) return true; + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_ARR && + options->kind != SCR_DYN_FUNC && options->kind != SCR_DYN_TYPED_REF && + options->kind != SCR_DYN_HANDLE) { + scr_dyn_arg_type_fail("options", "of type object", options); + return false; + } + ScrDyn *value = scr_dyn_obj_read(options, "stream", 6); + if (!value) return false; + *stream = scr_dyn_truthy(value); + scr_dyn_release(value); + return !scr_exc_pending(); +} + +static ScrDyn *scr_tcd_decoder_invoke(void *rec, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)self; + if (strcmp(method, "decode") != 0) { + scr_tcd_codec_not_a_function(what); + return NULL; + } + ScrBytes *input = scr_bytes_buffer_source(scr_tcd_codec_arg(args, argc, 0)); + if (!input) return NULL; + bool stream; + if (!scr_tcd_decoder_stream_option(scr_tcd_codec_arg(args, argc, 1), &stream)) { + scr_bytes_release(input); + return NULL; + } + const ScrTextCodecLayout *layout = &scr_tcd_decoder_layout; + ScrDyn *state; + memcpy(&state, (const char *)rec + layout->state, sizeof state); + ScrStr *text = scr_text_decode_stream(state, input, scr_tcd_decoder_encoding(rec), + scr_tcd_decoder_flag(rec, layout->fatal), + scr_tcd_decoder_flag(rec, layout->ignore_bom), stream); + scr_bytes_release(input); + return text ? scr_tcd_str(text) : NULL; +} + +static ScrDyn *scr_tcd_decoder_get(void *rec, const char *key, size_t len) { + if (len == 8 && memcmp(key, "encoding", 8) == 0) + return scr_tcd_str(scr_text_decoder_name(scr_tcd_decoder_encoding(rec))); + if (len == 5 && memcmp(key, "fatal", 5) == 0) + return scr_dyn_new_bool(scr_tcd_decoder_flag(rec, scr_tcd_decoder_layout.fatal)); + if (len == 9 && memcmp(key, "ignoreBOM", 9) == 0) + return scr_dyn_new_bool(scr_tcd_decoder_flag(rec, scr_tcd_decoder_layout.ignore_bom)); + return NULL; +} + +static bool scr_tcd_codec_set(void *rec, const char *key, size_t len, const ScrDyn *value) { + (void)rec; + (void)key; + (void)len; + (void)value; + return false; +} + +static const ScrDynHandleMethod scr_tcd_encoder_methods[] = { + {.name = "encode", .length = 0}, + {.name = "encodeInto", .length = 2}, + {.name = NULL}, +}; + +static const ScrDynHandleMethod scr_tcd_decoder_methods[] = { + {.name = "decode", .length = 0}, + {.name = NULL}, +}; + +static const ScrDynHandleOps scr_tcd_encoder_ops = { + .cls = "TextEncoder", + .to_string_tag = "TextEncoder", + .retain = scr_tcd_encoder_retain, + .release = scr_tcd_encoder_release, + .invoke = scr_tcd_encoder_invoke, + .get = scr_tcd_encoder_get, + .set = scr_tcd_codec_set, + .json_empty_object = true, + .methods = scr_tcd_encoder_methods, + .invalid_this = "Value of \"this\" must be of type TextEncoder", +}; + +static const ScrDynHandleOps scr_tcd_decoder_ops = { + .cls = "TextDecoder", + .to_string_tag = "TextDecoder", + .retain = scr_tcd_decoder_retain, + .release = scr_tcd_decoder_release, + .invoke = scr_tcd_decoder_invoke, + .get = scr_tcd_decoder_get, + .set = scr_tcd_codec_set, + .json_empty_object = true, + .methods = scr_tcd_decoder_methods, + .invalid_this = "Value of \"this\" must be of type TextDecoder", +}; + +ScrDyn *scr_text_encoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *)) { + if (!scr_tcd_encoder_layout.retain) { + scr_tcd_encoder_layout.retain = retain; + scr_tcd_encoder_layout.release = release; + scr_dyn_handle_install(SCR_DYNH_TEXT_ENCODER, &scr_tcd_encoder_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_TEXT_ENCODER); +} + +ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t fatal, size_t ignore_bom, size_t state) { + if (!scr_tcd_decoder_layout.retain) { + scr_tcd_decoder_layout = (ScrTextCodecLayout){retain, release, encoding, fatal, ignore_bom, state}; + scr_dyn_handle_install(SCR_DYNH_TEXT_DECODER, &scr_tcd_decoder_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_TEXT_DECODER); +} + +void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const ScrDynPath *path, + const char *want) { + return scr_dyn_handle_unbox(d, decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER, path, + want); +} + +bool scr_text_codec_is(const ScrDyn *d, bool decoder) { + return d->kind == SCR_DYN_HANDLE && + d->v.handle.tag == (decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER); +} diff --git a/tests/corpus/7747-text-codec-untyped-slot.js b/tests/corpus/7747-text-codec-untyped-slot.js new file mode 100644 index 000000000..cdf8345da --- /dev/null +++ b/tests/corpus/7747-text-codec-untyped-slot.js @@ -0,0 +1,69 @@ +// TextEncoder and TextDecoder instances held in untyped storage (hono's +// StreamingApi keeps `this.encoder = new TextEncoder()` in a JavaScript +// class field) and called from dynamic code, with their identity, streaming +// state, properties, method values and argument errors. +class StreamingApi { + /** @type {any} */ encoder; + /** @type {any} */ decoder; + /** @type {any} */ latin; + /** @type {any} */ strict; + constructor() { + this.encoder = new TextEncoder(); + this.decoder = new TextDecoder(); + this.latin = new TextDecoder("latin1"); + this.strict = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + } + write(input) { + if (typeof input === "string") input = this.encoder.encode(input); + return input; + } +} + +const show = (error) => console.log(error.name, error.code); +const api = new StreamingApi(); +console.log(api.write("héllo 😀")); +console.log(api.encoder.encoding, typeof api.encoder, api.encoder === api.encoder); +console.log(String(api.encoder), JSON.stringify(api.encoder), JSON.stringify({ d: api.decoder })); +console.log(api.encoder.encode(), api.encoder.encode(42), api.encoder.encode(undefined)); +const dest = new Uint8Array(4); +console.log(api.encoder.encodeInto("héllo", dest), dest); +try { + api.encoder.encodeInto(1, dest); +} catch (error) { + show(error); +} + +console.log(api.decoder.encoding, api.decoder.fatal, api.decoder.ignoreBOM); +console.log(api.latin.encoding, api.strict.fatal, api.strict.ignoreBOM); +const euro = api.encoder.encode("€uro"); +const head = api.decoder.decode(euro.subarray(0, 2), { stream: true }); +console.log(JSON.stringify(head), api.decoder.decode(euro.subarray(2))); +console.log(api.latin.decode(new Uint8Array([0xe9, 0x41]))); +console.log(JSON.stringify(api.strict.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])))); +try { + api.strict.decode(new Uint8Array([0xff])); +} catch (error) { + show(error); +} +try { + api.decoder.decode("text"); +} catch (error) { + show(error); +} +console.log(JSON.stringify(api.decoder.decode()), JSON.stringify(api.decoder.decode(undefined, null))); + +const encode = api.encoder.encode; +try { + encode("x"); +} catch (error) { + console.log(error.name, error.code, error.message); +} +console.log(typeof api.encoder.encode, api.encoder.encode.length, api.encoder.encodeInto.length); +console.log(api.decoder.decode.length, encode === api.encoder.encode); +console.log(api.encoder.encode.call(api.encoder, "ab")); +console.log("encode" in api.encoder, "decode" in api.decoder, api.encoder.missing); + +console.log(api.encoder instanceof TextEncoder, api.decoder instanceof TextDecoder); +console.log(api.encoder instanceof TextDecoder, JSON.parse("{}") instanceof TextEncoder); +const codecs = [api.encoder, api.decoder]; +console.log(codecs.indexOf(api.decoder), codecs.includes(api.encoder)); diff --git a/tests/corpus/7749-text-codec-unknown-roundtrip.ts b/tests/corpus/7749-text-codec-unknown-roundtrip.ts new file mode 100644 index 000000000..14c35d2bb --- /dev/null +++ b/tests/corpus/7749-text-codec-unknown-roundtrip.ts @@ -0,0 +1,22 @@ +// TypeScript codecs crossing 'unknown' slots: boxed by reference and +// checked back, keeping identity and a decoder's streaming state. +const holder: { codec: unknown; decoder: unknown } = { + codec: new TextEncoder(), + decoder: new TextDecoder("utf-16le"), +}; + +const encoder = holder.codec as TextEncoder; +console.log(encoder.encode("typed"), encoder === (holder.codec as TextEncoder)); +const decoder = holder.decoder as TextDecoder; +const bytes = new Uint8Array([0x68, 0x00, 0x69]); +console.log(JSON.stringify(decoder.decode(bytes, { stream: true }))); +console.log(JSON.stringify(decoder.decode(new Uint8Array([0x00]))), decoder.encoding); + +function store(value: unknown): unknown[] { + return [value, value]; +} +const pair = store(encoder); +console.log(pair[0] === pair[1], pair[0] === holder.codec); + +const maybe: unknown = Math.random() > 2 ? 1 : new TextDecoder(); +if (typeof maybe === "object" && maybe !== null) console.log((maybe as TextDecoder).encoding); diff --git a/tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json b/tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json new file mode 100644 index 000000000..a537f9ede --- /dev/null +++ b/tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 6500 } diff --git a/tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json b/tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json new file mode 100644 index 000000000..aa8658007 --- /dev/null +++ b/tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index fb15e80a4..c5476328e 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -41,6 +41,7 @@ const features: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 57857d642..64ddd0213 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -42,6 +42,7 @@ const base: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, From 934d254a417f48df2264f26c2409104e6ce398d2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:44:35 -0500 Subject: [PATCH 20/45] Keep the inline-cache record list within the shape budget --- packages/runtime/src/scr_js.h | 11 ++++++---- packages/runtime/src/scr_js_object.test.c | 26 +++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 6fa20fdb8..1d874b31e 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -195,10 +195,13 @@ bool scr_js_truthy(ScrJsVal value); * index and which keys an object lacks. * * Each property site of compiled dynamic-tier code owns one zeroed, - * thread-local ScrJsIc. The code at the site is fixed: it tests entry 0 - * inline and calls a probe for everything else. Probes never run user - * code: they answer from plain data properties or return a miss (NULL / - * false) with nothing done, and the site then runs its generic path. + * thread-local ScrJsIc with static storage: the reset that frees a + * thread's shapes (at thread exit, and when a library session ends) clears + * every record that holds entries. The code at the site is fixed: it tests + * entry 0 inline and calls a probe for everything else. Probes never run + * user code: they answer from plain data properties or return a miss + * (NULL / false) with nothing done, and the site then runs its generic + * path. * * load entry: shape = receiver shape; target = NULL for an own property * (slot indexes the receiver's entries), else the shape of diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index a14aa9ff6..6b6934dea 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -24,11 +24,17 @@ static ScrDyn *object_with(const ScrStr *key, double n) { return object; } +/* Cache records have static storage, as the compiled sites' do: the reset + * at exit clears every record that holds entries. Each check starts from + * an empty record. */ +static ScrJsIc load_ic; +static ScrJsIc store_ic; + /* Whether a load probe answers `object.key` and returns `n`. */ static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) { - ScrJsIc ic; - memset(&ic, 0, sizeof ic); - ScrDyn *value = scr_js_load_probe(object, key, &ic); + ScrJsIc *ic = &load_ic; + memset(ic, 0, sizeof *ic); + ScrDyn *value = scr_js_load_probe(object, key, ic); if (!value) return false; assert(value->kind == SCR_DYN_NUM && value->v.num == n); scr_dyn_release(value); @@ -66,14 +72,13 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { /* A store probe that would add a new shape leaves the add to the generic * store; one that replaces through an existing shape still runs. */ - ScrJsIc ic; - memset(&ic, 0, sizeof ic); + memset(&store_ic, 0, sizeof store_ic); ScrStr *added = scr_str_new("added", 5); ScrDyn *value = scr_dyn_new_num(8); - assert(!scr_js_store_probe(object, added, value, &ic)); + assert(!scr_js_store_probe(object, added, value, &store_ic)); assert(object->v.obj.len == 1); - memset(&ic, 0, sizeof ic); - assert(scr_js_store_probe(object, first, value, &ic)); + memset(&store_ic, 0, sizeof store_ic); + assert(scr_js_store_probe(object, first, value, &store_ic)); assert(probe_reads(object, first, 8)); scr_dyn_release(value); scr_str_release(added); @@ -90,10 +95,9 @@ static void immortal_objects_stay_unshaped(void) { object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); assert(object->v.obj.shape == NULL); - ScrJsIc ic; - memset(&ic, 0, sizeof ic); + memset(&store_ic, 0, sizeof store_ic); ScrDyn *value = scr_dyn_new_num(4); - assert(!scr_js_store_probe(object, key, value, &ic)); + assert(!scr_js_store_probe(object, key, value, &store_ic)); assert(object->v.obj.shape == NULL); scr_dyn_release(value); scr_str_release(key); From 0a98779dd979c1bc2a0d2a18dbda6fd4d1e8944b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:49:22 -0500 Subject: [PATCH 21/45] Convert arrays and records that hold text codecs to unknown The recursive boxability check rejected TextEncoder and TextDecoder records, so an array or record holding a codec could not convert to 'unknown' (SC1101), although the converter boxes a codec as its own handle at any depth. Codec records are now boxable there, and a live view of a record or array boxes a codec field or element as its handle too, instead of a view of the codec's internal fields. --- .../src/backend/llvm/expr-stream-bridges.ts | 8 +++++++- packages/compiler/src/ir/ir.ts | 3 ++- tests/corpus/7749-text-codec-unknown-roundtrip.ts | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a2f7208ec..70a8dcdee 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -722,7 +722,13 @@ export function streamTypedRefBoxValue( B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${value})`); return boxed; } - if (t.kind === "bytes" || (!streamTypedRefEligible(t) && !isDynTypedRefType(t))) { + // A codec record boxes as its own handle, by reference (toDynHelper), as + // it does outside a live view. + if ( + t.kind === "bytes" || + textCodecRecordClass(t, (id) => host.recordsById.get(id)) !== null || + (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) + ) { const valueTy = host.llType(t); B.line(`${boxed} = call ptr @${host.dyn.toDynHelper(t)}(${valueTy} ${value})`); return boxed; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 29dda102d..03b323cd5 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -4412,7 +4412,8 @@ function canBoxDynCompositeAt( case "record": { const shape = getRecord(t.shapeId); if (!shape) return false; - if (recordTextCodecClass(shape) !== null) return false; + // A codec boxes as its own handle wherever it sits (toDynHelper). + if (recordTextCodecClass(shape) !== null) return true; // Recursive shapes answer coinductively, like isJsonSafeType. if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); diff --git a/tests/corpus/7749-text-codec-unknown-roundtrip.ts b/tests/corpus/7749-text-codec-unknown-roundtrip.ts index 14c35d2bb..b7c18b3ef 100644 --- a/tests/corpus/7749-text-codec-unknown-roundtrip.ts +++ b/tests/corpus/7749-text-codec-unknown-roundtrip.ts @@ -20,3 +20,17 @@ console.log(pair[0] === pair[1], pair[0] === holder.codec); const maybe: unknown = Math.random() > 2 ? 1 : new TextDecoder(); if (typeof maybe === "object" && maybe !== null) console.log((maybe as TextDecoder).encoding); + +// Codecs inside arrays and records cross as their handles too. +const codecs: TextEncoder[] = [encoder, new TextEncoder()]; +const asUnknown: unknown = codecs; +const back = asUnknown as TextEncoder[]; +console.log(back.length, back[0] === encoder, back[1]!.encode("x")); +const streaming = new TextDecoder(); +const nested: { name: string; decoder: TextDecoder } = { name: "nested", decoder: streaming }; +const boxed: unknown = nested; +const unboxed = boxed as { name: string; decoder: TextDecoder }; +console.log(unboxed.name, unboxed.decoder === streaming); +console.log(JSON.stringify(unboxed.decoder.decode(new Uint8Array([0xe2, 0x82]), { stream: true }))); +console.log(JSON.stringify(streaming.decode(new Uint8Array([0xac])))); +console.log(JSON.stringify(boxed)); From 693e33742ac334e3778872f3478d4f70aafd18b1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:59:28 -0500 Subject: [PATCH 22/45] Support the raw-public and raw-private key formats EC and Ed25519 keys read and write Node 24's raw formats: - generateKeyPair(Sync) encodings: raw-public gives the EC point, uncompressed or, with type "compressed", compressed, or the Ed25519 public key; raw-private gives the EC scalar at the curve's size or the Ed25519 seed. - KeyObject.export({ format: "raw-public" | "raw-private" | "raw-seed" }), with Node's check of the point type, and Node's error for a passphrase with any format other than PEM and DER, which now also covers JWK. - createPublicKey and createPrivateKey of { key, format, asymmetricKeyType, namedCurve }, in Node's argument order, with compressed EC points, and createPublicKey of a raw private key. As in Node, raw-seed and every raw format of an RSA, RSA-PSS, DSA or DH key throw ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, which generateKeyPair passes to its callback. Invalid key data throws ERR_INVALID_ARG_VALUE. Raw X25519, X448, Ed448 and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. --- .../limitations/builtin-apis/crypto.mdx | 4 +- .../corpus/7449-crypto-raw-keys.mjs.json | 6 + packages/runtime/src/scr_pki.c | 252 +++++++++++++++--- tests/corpus/7449-crypto-raw-keys.mjs | 120 +++++++++ .../7449-crypto-raw-keys.mjs.json | 1 + 5 files changed, 352 insertions(+), 31 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json create mode 100644 tests/corpus/7449-crypto-raw-keys.mjs create mode 100644 tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index e4b555806..964ca4ec9 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -33,9 +33,11 @@ Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. - RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error. + RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + EC and Ed25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. + Raw X25519, X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json new file mode 100644 index 000000000..48bf2dfcc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7449-crypto-raw-keys.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index a1a5e97c2..94a0000e7 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -5,8 +5,9 @@ * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, * and the Sign and Verify objects of createSign and createVerify. Keys read * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, - * and JWK; the EC curves are prime256v1, secp384r1, secp521r1 and - * secp256k1, and the Ed25519 arithmetic is scr_ed25519.c. Argument checks + * JWK, and the raw-public and raw-private encodings; the EC curves are + * prime256v1, secp384r1, secp521r1 and secp256k1, and the Ed25519 + * arithmetic is scr_ed25519.c. Argument checks * and errors follow Node 24's lib/internal/crypto/keys.js, keygen.js and * sig.js. Encrypted keys and other key types throw a coded SC2020 error. * All arguments are borrowed. */ @@ -878,6 +879,8 @@ static ScrBytes *sp_source_bytes(const ScrDyn *v, const ScrDyn *encoding) { return NULL; } +static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const ScrDyn *format, bool private_input); + static const char sp_key_input_types[] = "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, or URL"; @@ -929,6 +932,14 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { SpKey *k = sp_key_from_jwk(data, private_key); return k ? sp_key_box(k) : NULL; } + if (sp_is_str(format, "raw-public") || sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed")) { + SpKey *k = sp_key_from_raw(input, data, format, private_key); + if (!k || private_key || !k->private_key) return k ? sp_key_box(k) : NULL; + SpKey *pub = sp_public_of(k); + sp_key_release(k); + if (!pub) sp_unsupported_input(); + return pub ? sp_key_box(pub) : NULL; + } if (!sp_absent(format) && !sp_is_str(format, "pem") && !sp_is_str(format, "der")) { sp_property_invalid("key.format", format); return NULL; @@ -959,6 +970,183 @@ ScrDyn *scr_crypto_create_private_key(ScrDyn *key) { return sp_create_key(key, t ScrDyn *scr_crypto_create_public_key(ScrDyn *key) { return sp_create_key(key, false); } +/* ── raw keys ─────────────────────────────────────────────────────────── */ + +/* A namedCurve as Node reads it: the OpenSSL short name or the NIST name, + * matched exactly. NULL for other names. */ +static const SpCurve *sp_curve_named(const ScrDyn *name) { + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(name, sp_curves[i].name) || (sp_curves[i].nist && sp_is_str(name, sp_curves[i].nist))) + return &sp_curves[i]; + return NULL; +} + +static void sp_incompatible_key_type(void) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding is incompatible with the key type"); +} + +static void sp_invalid_key_data(void) { sp_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid key data"); } + +/* A key's raw encoding: an EC public point, compressed when asked, or the + * private scalar at the curve's size, or an Ed25519 public key or seed. + * raw-seed, and every encoding of an RSA key, throw + * ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS. NULL with the error pending. */ +static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compressed) { + if (seed || sp_is_rsa(k)) { + sp_incompatible_key_type(); + return NULL; + } + if (sp_is_ed(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + const mbedtls_ecp_group *grp = &ec->MBEDTLS_PRIVATE(grp); + unsigned char buf[1 + 2 * 66]; + size_t n = (grp->nbits + 7) / 8; + int rc = private_part ? mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), buf, n) + : mbedtls_ecp_point_write_binary( + grp, &ec->MBEDTLS_PRIVATE(Q), + compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED, &n, buf, sizeof buf); + ScrDyn *out = rc == 0 ? sp_dyn_buffer(buf, n) : NULL; + mbedtls_platform_zeroize(buf, sizeof buf); + if (!out) sp_unsupported_input(); + return out; +} + +/* Node's named key types that this runtime does not generate or read: + * the other OKP curves and the post-quantum types. */ +static const char *const sp_other_named_types[] = { + "ed448", + "x25519", + "x448", + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", + "ml-kem-512", + "ml-kem-768", + "ml-kem-1024", + "slh-dsa-sha2-128f", + "slh-dsa-sha2-128s", + "slh-dsa-sha2-192f", + "slh-dsa-sha2-192s", + "slh-dsa-sha2-256f", + "slh-dsa-sha2-256s", + "slh-dsa-shake-128f", + "slh-dsa-shake-128s", + "slh-dsa-shake-192f", + "slh-dsa-shake-192s", + "slh-dsa-shake-256f", + "slh-dsa-shake-256s", +}; + +/* createPrivateKey / createPublicKey of { key, format: "raw-public" | + * "raw-private" | "raw-seed", asymmetricKeyType, namedCurve }: an EC point + * (uncompressed or compressed) or scalar, or an Ed25519 public key or + * seed. The argument checks are Node's prepareAsymmetricKey order, then + * the key type, the seed format, the curve and the data. NULL with the + * error pending. */ +static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const ScrDyn *format, bool private_input) { + bool raw_public = sp_is_str(format, "raw-public"); + bool seed = sp_is_str(format, "raw-seed"); + if (private_input && raw_public) { + sp_property_invalid("key.format", format); + return NULL; + } + if (data->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(data)) { + scr_dyn_arg_type_fail("key.key", "an instance of ArrayBuffer, Buffer, TypedArray, or DataView", data); + return NULL; + } + const ScrDyn *type = sp_opt(input, "asymmetricKeyType"); + if (type->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.asymmetricKeyType", "of type string", type); + return NULL; + } + bool ec = sp_is_str(type, "ec"); + bool ed = sp_is_str(type, "ed25519"); + const ScrDyn *curve_name = sp_opt(input, "namedCurve"); + if (ec && curve_name->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.namedCurve", "of type string", curve_name); + return NULL; + } + if (sp_is_str(type, "rsa") || sp_is_str(type, "rsa-pss") || sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { + sp_incompatible_key_type(); + return NULL; + } + bool other = false; + for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) + other = other || sp_is_str(type, sp_other_named_types[i]); + if (!ec && !ed && !other) { + ScrStr *s = type->v.str; + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid asymmetricKeyType: %.*s", (int)s->len, s->data); + return NULL; + } + bool ml = !ec && !ed && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; + if (seed && !ml) { + sp_incompatible_key_type(); + return NULL; + } + if (other) { + sp_not_supported("raw keys of this asymmetricKeyType"); + return NULL; + } + const SpCurve *curve = NULL; + if (ec && !(curve = sp_curve_named(curve_name))) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + ScrBytes *bytes = scr_bytes_buffer_source(data); + if (!bytes) return NULL; + const unsigned char *p = bytes->data; + size_t len = (size_t)scr_bytes_byte_len(bytes); + SpKey *k = sp_key_alloc(!raw_public); + bool ok = false; + if (ed) { + k->ed25519 = true; + if (len == 32 && raw_public) { + memcpy(k->ed_pub, p, 32); + ok = true; + } else if (len == 32) { + const ScrPkiEd25519 *eds = sp_ed(); + if (!eds) { + sp_key_release(k); + scr_bytes_release(bytes); + return NULL; + } + memcpy(k->ed_seed, p, 32); + eds->public_key(k->ed_pub, k->ed_seed); + ok = true; + } + } else if (raw_public && len == 1 && p[0] == 0) { + sp_key_release(k); + scr_bytes_release(bytes); + sp_not_supported("EC public keys at the point at infinity"); + return NULL; + } else if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *kp = mbedtls_pk_ec(k->pk); + mbedtls_ecp_group *grp = &kp->MBEDTLS_PRIVATE(grp); + ok = mbedtls_ecp_group_load(grp, curve->id) == 0; + if (ok && raw_public) { + ok = mbedtls_ecp_point_read_binary(grp, &kp->MBEDTLS_PRIVATE(Q), p, len) == 0 && + mbedtls_ecp_check_pubkey(grp, &kp->MBEDTLS_PRIVATE(Q)) == 0; + } else if (ok) { + ok = len == (grp->nbits + 7) / 8 && mbedtls_mpi_read_binary(&kp->MBEDTLS_PRIVATE(d), p, len) == 0; + if (ok && mbedtls_ecp_check_privkey(grp, &kp->MBEDTLS_PRIVATE(d)) != 0) { + sp_key_release(k); + scr_bytes_release(bytes); + sp_not_supported("EC private keys outside 1 to n - 1"); + return NULL; + } + ok = ok && mbedtls_ecp_mul(grp, &kp->MBEDTLS_PRIVATE(Q), &kp->MBEDTLS_PRIVATE(d), &grp->G, sp_random, NULL) == 0; + } + } + scr_bytes_release(bytes); + if (!ok) { + sp_key_release(k); + sp_invalid_key_data(); + return NULL; + } + return k; +} + /* ── export ───────────────────────────────────────────────────────────── */ /* export({ format, type }) per Node's parseKeyEncoding: format "pem", @@ -975,11 +1163,37 @@ static ScrDyn *sp_export(SpKey *k, const ScrDyn *options) { } static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { + const ScrDyn *format = sp_opt(options, "format"); + /* A private key's passphrase applies only to PEM and DER. */ + if (k->private_key && !sp_absent(sp_opt(options, "passphrase")) && !sp_is_str(format, "pem") && + !sp_is_str(format, "der")) { + ScrStr *text = scr_dyn_string_coerce_js(format); + if (!text) return NULL; + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding %.*s does not support encryption.", (int)text->len, text->data); + scr_str_release(text); + return NULL; + } + if (!k->private_key && sp_is_str(format, "raw-public")) { + const ScrDyn *type = sp_opt(options, "type"); + bool compressed = sp_is_str(type, "compressed"); + /* Only an EC point has a compressed form. */ + if (sp_is_ec(k) && !sp_absent(type) && !compressed && !sp_is_str(type, "uncompressed")) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The property 'options.type' must be one of: 'compressed', 'uncompressed'. Received %.*s", + (int)received->len, received->data); + scr_str_release(received); + return NULL; + } + return sp_export_raw(k, false, false, sp_is_ec(k) && compressed); + } + if (k->private_key && (sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed"))) + return sp_export_raw(k, true, sp_is_str(format, "raw-seed"), false); if (options == NULL || !sp_is_object(options)) { scr_dyn_arg_type_fail("options", "of type object", options ? options : scr_dyn_undefined()); return NULL; } - const ScrDyn *format = sp_opt(options, "format"); if (sp_is_str(format, "jwk")) return sp_export_jwk(k); bool pem = sp_is_str(format, "pem"); if (!pem && !sp_is_str(format, "der")) { @@ -1227,7 +1441,7 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S /* Node's parseKeyEncoding for options.publicKeyEncoding or * options.privateKeyEncoding of a `type` key: true when the encoding is - * undefined, null or valid, false with the error pending. A valid raw or + * undefined, null or valid, false with the error pending. A valid * encrypted encoding, which this runtime does not write, sets *refusal * when it is unset. */ static bool sp_check_encoding(const ScrDyn *options, const char *key, const ScrDyn *type, bool private_key, @@ -1323,7 +1537,6 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S return false; } } - if (!*refusal && (raw_public || raw_private)) *refusal = "generateKeyPair with a raw key encoding"; if (!*refusal && encrypted) *refusal = "encrypted private key export"; return true; } @@ -1335,31 +1548,6 @@ typedef struct { const SpCurve *curve; /* EC */ } SpKeyGen; -/* The key types Node generates that this runtime does not, apart from - * rsa-pss, dsa and dh: Node's named key pairs. */ -static const char *const sp_other_named_types[] = { - "ed448", - "x25519", - "x448", - "ml-dsa-44", - "ml-dsa-65", - "ml-dsa-87", - "ml-kem-512", - "ml-kem-768", - "ml-kem-1024", - "slh-dsa-sha2-128f", - "slh-dsa-sha2-128s", - "slh-dsa-sha2-192f", - "slh-dsa-sha2-192s", - "slh-dsa-sha2-256f", - "slh-dsa-sha2-256s", - "slh-dsa-shake-128f", - "slh-dsa-shake-128s", - "slh-dsa-shake-192f", - "slh-dsa-shake-192s", - "slh-dsa-shake-256f", - "slh-dsa-shake-256s", -}; /* createJob's checks for generateKeyPair(Sync)(type, options) in Node's * order: the type, the options object, the public and private key @@ -1502,14 +1690,18 @@ static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_ sp_unsupported_input(); return NULL; } + /* An encoding the key type has no form for (a raw encoding of an RSA + * key, raw-seed) fails the job, as in Node. */ ScrDyn *public_value = sp_encoded(pub, sp_opt(options, "publicKeyEncoding")); if (!public_value) { sp_key_release(k); + *job_error = true; return NULL; } ScrDyn *private_value = sp_encoded(k, sp_opt(options, "privateKeyEncoding")); if (!private_value) { scr_dyn_release(public_value); + *job_error = true; return NULL; } ScrDyn *pair = scr_dyn_new_obj(); diff --git a/tests/corpus/7449-crypto-raw-keys.mjs b/tests/corpus/7449-crypto-raw-keys.mjs new file mode 100644 index 000000000..0795dbb0b --- /dev/null +++ b/tests/corpus/7449-crypto-raw-keys.mjs @@ -0,0 +1,120 @@ +// The raw-public, raw-private and raw-seed key formats: generateKeyPair +// encodings, KeyObject.export and createPublicKey/createPrivateKey with +// asymmetricKeyType and namedCurve, for EC keys (uncompressed and +// compressed points, scalars at the curve's size) and Ed25519 keys, and +// Node's errors for the other key types and formats. +import * as crypto from "node:crypto"; + +function describe(value) { + if (Buffer.isBuffer(value)) return `Buffer(${value.length})`; + if (value instanceof crypto.KeyObject) return `${value.type} ${value.asymmetricKeyType} ${JSON.stringify(value.asymmetricKeyDetails)}`; + if (value && typeof value === "object") return Object.entries(value).map(([key, field]) => `${key}=${describe(field)}`).join(" "); + return String(value); +} + +function attempt(label, run) { + try { + console.log(label, describe(run())); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +// generateKeyPair encodings +for (const namedCurve of ["P-256", "P-384", "P-521", "secp256k1"]) { + attempt(`ec ${namedCurve} raw-public`, () => crypto.generateKeyPairSync("ec", { namedCurve, publicKeyEncoding: { format: "raw-public" } })); + attempt(`ec ${namedCurve} compressed`, () => crypto.generateKeyPairSync("ec", { namedCurve, publicKeyEncoding: { format: "raw-public", type: "compressed" } })); + attempt(`ec ${namedCurve} raw-private`, () => crypto.generateKeyPairSync("ec", { namedCurve, privateKeyEncoding: { format: "raw-private" } })); +} +attempt("ec raw-seed", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { format: "raw-seed" } })); +attempt("ed25519 raw", () => + crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: { format: "raw-public" }, privateKeyEncoding: { format: "raw-private" } }), +); +attempt("ed25519 raw-seed", () => crypto.generateKeyPairSync("ed25519", { privateKeyEncoding: { format: "raw-seed" } })); +attempt("rsa raw-public", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicKeyEncoding: { format: "raw-public" } })); + +// export +const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const ed = crypto.generateKeyPairSync("ed25519"); +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const point = Buffer.from(ec.publicKey.export({ format: "raw-public" })); +const compressed = Buffer.from(ec.publicKey.export({ format: "raw-public", type: "compressed" })); +const scalar = Buffer.from(ec.privateKey.export({ format: "raw-private" })); +const jwk = ec.privateKey.export({ format: "jwk" }); +console.log("ec point", point.length, point[0], point.subarray(1, 33).toString("base64url") === jwk.x, point.subarray(33).toString("base64url") === jwk.y); +console.log("ec compressed", compressed.length, compressed[0] === 2 + (point[64] & 1), compressed.subarray(1).equals(point.subarray(1, 33))); +console.log("ec scalar", scalar.length, scalar.toString("base64url") === jwk.d); +console.log("ec uncompressed", Buffer.from(ec.publicKey.export({ format: "raw-public", type: "uncompressed" })).equals(point)); +const edPublic = Buffer.from(ed.publicKey.export({ format: "raw-public" })); +const edSeed = Buffer.from(ed.privateKey.export({ format: "raw-private" })); +const edJwk = ed.privateKey.export({ format: "jwk" }); +console.log("ed25519", edPublic.toString("base64url") === edJwk.x, edSeed.toString("base64url") === edJwk.d); +attempt("ed25519 raw-public with a type", () => ed.publicKey.export({ format: "raw-public", type: "x" })); +attempt("ec bad point type", () => ec.publicKey.export({ format: "raw-public", type: "x" })); +attempt("ec null point type", () => ec.publicKey.export({ format: "raw-public", type: null })); +attempt("private raw-public", () => ec.privateKey.export({ format: "raw-public" })); +attempt("public raw-private", () => ec.publicKey.export({ format: "raw-private" })); +attempt("ec raw-seed", () => ec.privateKey.export({ format: "raw-seed" })); +attempt("ed25519 raw-seed", () => ed.privateKey.export({ format: "raw-seed" })); +attempt("rsa raw-public", () => rsa.publicKey.export({ format: "raw-public" })); +attempt("rsa raw-private", () => rsa.privateKey.export({ format: "raw-private" })); +for (const format of [undefined, 5, "jwk", "raw-private", "raw-seed"]) + attempt(`passphrase with ${format}`, () => ec.privateKey.export({ format, passphrase: "x" })); + +// import +const pub = (key) => () => crypto.createPublicKey(key); +const priv = (key) => () => crypto.createPrivateKey(key); +attempt("ec point", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec compressed point", pub({ key: compressed, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "prime256v1" })); +attempt("ec scalar", priv({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec scalar as public", pub({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +const reread = crypto.createPublicKey({ key: compressed, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("ec reread", reread.equals(ec.publicKey), Buffer.from(reread.export({ format: "raw-public" })).equals(point)); +const rereadPrivate = crypto.createPrivateKey({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("ec private reread", crypto.createPublicKey(rereadPrivate).equals(ec.publicKey)); +attempt("ed25519 public", pub({ key: edPublic, format: "raw-public", asymmetricKeyType: "ed25519" })); +attempt("ed25519 seed", priv({ key: edSeed, format: "raw-private", asymmetricKeyType: "ed25519" })); +console.log("ed25519 reread", crypto.createPublicKey({ key: edSeed, format: "raw-private", asymmetricKeyType: "ed25519" }).equals(ed.publicKey)); + +// fixed keys: the EC scalar 1 is the generator, and an Ed25519 seed of +// zeros has a known public key +const one = Buffer.concat([Buffer.alloc(31), Buffer.from([1])]); +const generator = crypto.createPrivateKey({ key: one, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("generator", Buffer.from(crypto.createPublicKey(generator).export({ format: "raw-public", type: "compressed" })).toString("hex")); +const zeros = crypto.createPrivateKey({ key: Buffer.alloc(32), format: "raw-private", asymmetricKeyType: "ed25519" }); +console.log("zero seed", Buffer.from(crypto.createPublicKey(zeros).export({ format: "raw-public" })).toString("hex")); +const message = Buffer.from("raw keys"); +console.log("zero seed signature", crypto.sign(null, message, zeros).toString("hex").slice(0, 32)); + +// import errors +attempt("raw-public to createPrivateKey", priv({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("string data", pub({ key: "abc", format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("no asymmetricKeyType", pub({ key: point, format: "raw-public" })); +attempt("number asymmetricKeyType", pub({ key: point, format: "raw-public", asymmetricKeyType: 5 })); +attempt("no namedCurve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec" })); +attempt("unknown asymmetricKeyType", pub({ key: point, format: "raw-public", asymmetricKeyType: "nope" })); +attempt("Ed25519 spelling", pub({ key: edPublic, format: "raw-public", asymmetricKeyType: "Ed25519" })); +attempt("unknown curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "nope" })); +attempt("lower-case NIST curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "p-256" })); +attempt("other curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-384" })); +attempt("short point", pub({ key: point.subarray(0, 64), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("off-curve point", pub({ key: Buffer.concat([Buffer.from([4]), Buffer.alloc(64, 1)]), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("bad compressed x", pub({ key: Buffer.concat([Buffer.from([2]), Buffer.alloc(32, 0xff)]), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("short scalar", priv({ key: scalar.subarray(0, 31), format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("long scalar", priv({ key: Buffer.concat([Buffer.alloc(1), scalar]), format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec raw-seed", priv({ key: scalar, format: "raw-seed", asymmetricKeyType: "ec", namedCurve: "nope" })); +attempt("ed25519 raw-seed", priv({ key: edSeed, format: "raw-seed", asymmetricKeyType: "ed25519" })); +attempt("short ed25519 key", pub({ key: edPublic.subarray(0, 31), format: "raw-public", asymmetricKeyType: "ed25519" })); +attempt("short ed25519 seed", priv({ key: edSeed.subarray(0, 31), format: "raw-private", asymmetricKeyType: "ed25519" })); +for (const asymmetricKeyType of ["rsa", "rsa-pss", "dsa", "dh"]) + attempt(`${asymmetricKeyType} raw`, pub({ key: edPublic, format: "raw-public", asymmetricKeyType })); + +// generateKeyPair passes an encoding the key type has no form for to its +// callback +crypto.generateKeyPair("ed25519", { privateKeyEncoding: { format: "raw-seed" } }, (...args) => { + console.log("async raw-seed", args.length, args[0].code); + crypto.generateKeyPair("ec", { namedCurve: "P-384", publicKeyEncoding: { format: "raw-public", type: "compressed" } }, (err, publicKey, privateKey) => { + console.log("async raw-public", err, describe(publicKey), describe(privateKey)); + }); +}); +console.log("returned"); diff --git a/tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json b/tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json new file mode 100644 index 000000000..c8b39ec8a --- /dev/null +++ b/tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json @@ -0,0 +1 @@ +{ "plain": 7592, "san": 12217 } From c24ae2d7e921eb70d92d435f287ddde12d1123f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:11:21 -0500 Subject: [PATCH 23/45] Keep caches for property helpers that answer plain values first Property helpers now start with an exit for values no class table matches: if the receiver is neither a class capsule nor a function, they run the generic read or write on it directly. The inline-cache recognizer accepted only capsule branches and the subclass normalization before a helper's generic tail, so these helpers stopped being cache sites. It now also accepts that exit when its operation is the one the site caches: a read or write of the same key on the receiver. --- packages/compiler/src/ir/js-ic.test.ts | 54 ++++++++++++++++++++ packages/compiler/src/ir/js-ic.ts | 69 ++++++++++++++++++++++---- 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index 026600cc3..f5391b83e 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -371,3 +371,57 @@ test("bag helpers that exit early for values no class branch matches are still t expect(sites.site("%dyn.class.property.2")).toBeNull(); expect(sites.site("%dyn.class.property.3")).toBeNull(); }); + +test("helpers that answer plain values first with the same operation are cache sites", () => { + const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({ + kind: "dynTest", + test, + negated: true, + value: ref("p.0"), + type: BOOL, + loc, + }); + const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({ + kind: "if", + cond: test, + then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }], + else_: null, + loc, + }); + const store = (key: string): IrStmt[] => [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ref("p.0"), str(key), ref("p.1")], + type: VOID, + loc, + }, + loc, + }, + ret(null), + ]; + const load = (key: string): IrStmt[] => [ + ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }), + ]; + const sites = sitesOf( + setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]), + getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]), + // An exit for another key, or one plain values do not take. + setHelper("%dyn.class.property.2", "x", [exit(store("y"))]), + setHelper("%dyn.class.property.3", "x", [ + exit(store("x"), { + kind: "dynTest", + test: "classCapsule", + value: ref("p.0"), + type: BOOL, + loc, + }), + ]), + ); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); + expect(sites.site("%dyn.class.property.3")).toBeNull(); +}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 58c568bbd..48019636a 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -61,17 +61,68 @@ export class JsIcSites { if (fn.params.some((p) => p.type.kind !== "dyn")) return null; const receiver = fn.params[0]?.localId; if (receiver === undefined) return null; + const second = fn.params[1]?.localId; + const exits: JsIcSite[] = []; let i = 0; - while ( - i < fn.body.length && - (this.capsuleBranch(fn.body[i]!, receiver) || this.rebinding(fn.body[i]!, receiver)) - ) - i++; + for (; i < fn.body.length; i++) { + const s = fn.body[i]!; + if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue; + const exit = this.plainExit(s, receiver, second); + if (exit === null) break; + exits.push(exit); + } const tail = fn.body.slice(i); - if (fn.params.length === 1) return this.getTail(tail, receiver); - if (fn.params.length !== 2) return null; - const second = fn.params[1]!.localId; - return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); + let site: JsIcSite | null; + if (fn.params.length === 1) site = this.getTail(tail, receiver); + else if (fn.params.length !== 2 || second === undefined) return null; + else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); + if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null; + // An exit answers plain values with the operation the site caches. + return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null; + } + + /** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no + * else: an exit that runs the helper's generic operation directly on a + * value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R, + * K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a + * bag of it). */ + private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null { + const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean => + e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver); + if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null; + let body = s.then; + const inner = body[0]; + if ( + body.length === 1 && + inner?.kind === "if" && + inner.else_ === null && + negated(inner.cond, "funcKind") + ) + body = inner.then; + const [first, ret] = body; + if (body.length === 1 && first?.kind === "return") { + const read = unwrapFromDyn(first.value); + if ( + read?.kind !== "dynKeyGet" || + read.key.kind !== "strLit" || + !this.bagOf(read.value, receiver) + ) + return null; + return { kind: "get", key: read.key.value }; + } + if ( + body.length !== 2 || + first?.kind !== "exprStmt" || + ret?.kind !== "return" || + ret.value !== null + ) + return null; + const call = first.expr; + if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null; + const [bag, key, stored] = call.args; + if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null; + if (value === undefined || !isVar(stored, value)) return null; + return { kind: "set", key: key.value }; } /** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ? From c2963628343abb47685249cedaa2613bb612c888 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:21:21 -0500 Subject: [PATCH 24/45] Park typed collection iterators on the entry they read An early exit from a loop over an iterator-typed Map or Set method parks the iterator with a second yield of the current entry. That yield read the entry again by its index, which aborted with a map iteration error when the loop had deleted the entry. Each step now reads the entry once into a local that both yields answer. `entries()` takes the typed walk only for a slot whose yield type is the key and value pair record. Other yield types, such as `IterableIterator` or ``, keep the checked adapter instead of failing to compile. --- .../frontend/lowering/iterator-adapters.ts | 28 +++++++++- .../7746-key-order-iterable-iterator.ts | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index a8b4ea496..9f2b1e31f 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -84,6 +84,26 @@ function collectionIteratorSlotValue( collT.kind === "map" ? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc } : { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc }; + // entries() answers [key, value] pairs: only a slot whose yield type is + // that pair as a record of the two fields takes the typed walk; a tuple + // or other pair type keeps the checked adapter. + if (method === "entries") { + const pair = + expected.yieldT.kind === "record" ? lowerer.shapes.get(expected.yieldT.shapeId) : undefined; + const field = (name: string): IrType | undefined => + pair?.fields.find((f) => f.name === name)?.type; + const first = field("0"); + const second = field("1"); + if ( + pair === undefined || + pair.fields.length !== 2 || + first === undefined || + second === undefined || + !typeEquals(first, keyT) || + !typeEquals(second, valueT) + ) + return null; + } const entry = (): IrExpr => method === "keys" || (collT.kind === "set" && method === "values") ? read("iterKey", keyT) @@ -99,9 +119,13 @@ function collectionIteratorSlotValue( loc, }; if (!typeEquals(entry().type, expected.yieldT)) return null; + // The entry is read once per step and kept: the parking yield of an + // early exit answers it again rather than rereading a slot the loop may + // have deleted. + const current = varRef("current.0", expected.yieldT, loc); const suspend = (): IrStmt => ({ kind: "exprStmt", - expr: { kind: "yieldExpr", value: entry(), type: resumeType(expected.nextT), loc }, + expr: { kind: "yieldExpr", value: current, type: resumeType(expected.nextT), loc }, loc, }); const finish: IrStmt = { @@ -120,6 +144,7 @@ function collectionIteratorSlotValue( { id: "native.0", name: "native", type: DYN, mutable: false }, { id: "index.0", name: "index", type: F64, mutable: true }, { id: "resumed.0", name: "resumed", type: BOOL, mutable: true }, + { id: "current.0", name: "current", type: expected.yieldT, mutable: false }, ], body: [ { @@ -173,6 +198,7 @@ function collectionIteratorSlotValue( loc, }, setResumed(false, loc), + { kind: "varDecl", localId: "current.0", init: entry(), loc }, { kind: "tryCatch", tryBody: [suspend(), setResumed(true, loc)], diff --git a/tests/corpus/7746-key-order-iterable-iterator.ts b/tests/corpus/7746-key-order-iterable-iterator.ts index 13f27ee83..d9d4ea15a 100644 --- a/tests/corpus/7746-key-order-iterable-iterator.ts +++ b/tests/corpus/7746-key-order-iterable-iterator.ts @@ -56,3 +56,54 @@ const ordered = [...newest(listed)]; console.log(JSON.stringify(ordered), ordered[0] === listed[1]); const step = newest(listed).next(); console.log(step.done, JSON.stringify(step.value)); + +// Deleting the current entry before leaving a loop over an iterator-typed +// method: the iterator parks after that entry and continues with the next. +class Store { + readonly rows = new Map(); + readonly tags = new Set(); + values(): IterableIterator { + return this.rows.values(); + } + keys(): IterableIterator { + return this.rows.keys(); + } + tagValues(): IterableIterator { + return this.tags.values(); + } + anyPairs(): IterableIterator { + return this.rows.entries(); + } + unknownPairs(): IterableIterator { + return this.rows.entries(); + } + tagAny(): IterableIterator { + return this.tags.entries(); + } +} +const store = new Store(); +for (const [k, v] of [["a", 1], ["b", 2], ["c", 3]] as const) store.rows.set(k, v); +for (const t of ["x", "y", "z"]) store.tags.add(t); +console.log(JSON.stringify([...store.anyPairs()]), JSON.stringify([...store.unknownPairs()])); +console.log(JSON.stringify([...store.tagAny()])); +const values = store.values(); +for (const v of values) { + if (v === 2) { + store.rows.delete("b"); + break; + } +} +console.log([...values].join(), [...store.rows.keys()].join()); +for (const k of store.keys()) { + store.rows.delete(k); + break; +} +console.log([...store.rows.keys()].join()); +const tags = store.tagValues(); +for (const t of tags) { + if (t === "y") { + store.tags.delete("y"); + break; + } +} +console.log([...tags].join(), [...store.tags].join()); From 89d122574f708d0bede331b8b21a9fd3505fbd35 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:27:20 -0500 Subject: [PATCH 25/45] Run Test262 tests that include testTypedArray.js --- tests/harness/test262-runner.test.ts | 20 + tests/harness/test262.test.ts | 69 +++ tests/test262/README.md | 4 +- tests/test262/run.ts | 6 +- tests/test262/support.mjs | 12 +- tests/test262/test-typed-array.js | 313 +++++++++++++ tests/test262/upstream.json | 13 + .../test262/vendor/harness/testTypedArray.js | 431 ++++++++++++++++++ .../prototype/byteOffset/return-byteoffset.js | 31 ++ .../return-abrupt-from-begin-symbol.js | 24 + .../byteoffset-is-negative-zero-sab.js | 23 + .../is-infinity-throws-rangeerror.js | 27 ++ .../undefined-newtarget-throws.js | 27 ++ ...mericindex-desc-not-configurable-throws.js | 39 ++ 14 files changed, 1032 insertions(+), 7 deletions(-) create mode 100644 tests/test262/test-typed-array.js create mode 100644 tests/test262/vendor/harness/testTypedArray.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js diff --git a/tests/harness/test262-runner.test.ts b/tests/harness/test262-runner.test.ts index 6f6df35d4..3ddf3ca84 100644 --- a/tests/harness/test262-runner.test.ts +++ b/tests/harness/test262-runner.test.ts @@ -101,6 +101,26 @@ test("the property helper only admits direct inline descriptor checks", () => { } }); +test("the typed array helper is admitted without the %TypedArray% intrinsic", () => { + const text = source( + "includes: [testTypedArray.js]", + "testWithTypedArrayConstructors(function(TA) { assert.sameValue(new TA(1).length, 1); });", + ); + expect(exclusion(text, metadata(text), "strict")).toBeUndefined(); + const prepared = prepare(text, false, undefined, "strict", ["testTypedArray.js"]); + expect(prepared).toContain("function testWithTypedArrayConstructors("); + expect(prepared).not.toContain("Object.getPrototypeOf(Int8Array)"); + for (const body of [ + "assert.sameValue(typeof TypedArray, 'function');", + "testWithTypedArrayConstructors(function(TA) { assert.sameValue(Object.getPrototypeOf(TA), TypedArray); });", + ]) { + const named = source("includes: [testTypedArray.js]", body); + expect(exclusion(named, metadata(named), "strict")).toBe("harness:TypedArray-intrinsic"); + } + const other = source("includes: [testTypedArray.js, detachArrayBuffer.js]"); + expect(exclusion(other, metadata(other), "strict")).toBe("harness-includes:detachArrayBuffer.js"); +}); + test("receiver-bound this is admitted without adapting script-level this", () => { for (const [body, expected] of [ ["class C { read() { return this.value; } }", undefined], diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index a36fa2cdf..f1b616578 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -19,6 +19,10 @@ const upstreamHarness = ["assert.js", "sta.js"] .map((name) => readFileSync(join(vendorRoot, "harness", name), "utf8")) .join("\n"); const upstreamPropertyHelper = readFileSync(join(vendorRoot, "harness/propertyHelper.js"), "utf8"); +const upstreamTypedArrayHelper = readFileSync( + join(vendorRoot, "harness/testTypedArray.js"), + "utf8", +); function runUpstream( source: string, @@ -29,6 +33,8 @@ function runUpstream( runInContext(upstreamHarness, context, { timeout: 5000 }); if (includes.includes("propertyHelper.js")) runInContext(upstreamPropertyHelper, context, { timeout: 5000 }); + if (includes.includes("testTypedArray.js")) + runInContext(upstreamTypedArrayHelper, context, { timeout: 5000 }); runInContext(variant === "strict" ? `"use strict";\n${source}` : source, context, { timeout: 5000, }); @@ -52,6 +58,8 @@ async function runUpstreamAsync( runInContext(upstreamHarness, context, { timeout: 5000 }); if (includes.includes("propertyHelper.js")) runInContext(upstreamPropertyHelper, context, { timeout: 5000 }); + if (includes.includes("testTypedArray.js")) + runInContext(upstreamTypedArrayHelper, context, { timeout: 5000 }); runInContext(variant === "strict" ? `"use strict";\n${source}` : source, context, { timeout: 5000, }); @@ -375,6 +383,67 @@ if (propertyControls.length > 0) } }); +const typedArrayControls = shardSelect( + [ + { + name: "every constructor with array, array-like and buffer arguments", + status: "pass", + source: + "testWithTypedArrayConstructors(function(TA, makeCtorArg) { var sample = new TA(makeCtorArg([40, 41, 42])); assert.sameValue(sample.length, 3); assert.sameValue(sample[2], 42); }, null, ['passthrough', 'arraylike', 'arraybuffer']);", + }, + { + name: "failing element assertion", + status: "fail", + source: + "testWithTypedArrayConstructors(function(TA, makeCtorArg) { var sample = new TA(makeCtorArg([40, 41, 42])); assert.sameValue(sample[2], 43); });", + }, + { + name: "failure in one constructor", + status: "fail", + source: + "testWithTypedArrayConstructors(function(TA) { assert(TA !== Int16Array, 'Int16Array'); });", + }, + { + name: "passthrough factories visit each constructor once", + status: "pass", + source: + "var count = 0; testWithTypedArrayConstructors(function(TA, makeCtorArg) { assert.sameValue(new TA(makeCtorArg(4)).length, 4); count++; }, null, ['passthrough']); assert.sameValue(count, typedArrayConstructors.length);", + }, + { + name: "explicit constructor list", + status: "pass", + source: + "var names = []; testWithTypedArrayConstructors(function(TA) { names.push(TA.name); }, [Uint8Array, Float64Array], ['passthrough']); assert.compareArray(names, ['Uint8Array', 'Float64Array']);", + }, + { + name: "float constructor classification", + status: "pass", + source: + "assert(isFloatTypedArrayConstructor(Float32Array)); assert(!isFloatTypedArrayConstructor(Int8Array)); assert.sameValue(floatTypedArrayConstructorPrecision(Float64Array), 'double');", + }, + { + name: "unknown argument factory feature", + status: "fail", + source: "testWithTypedArrayConstructors(function() {}, null, ['unknown']);", + }, + ], + (item) => `typed-array:${item.name}`, +); + +if (typedArrayControls.length > 0) + describe(`Test262 typed array helper contract${shardSuffix()}`, () => { + for (const control of typedArrayControls) { + test(control.name, async () => { + const includes = ["compareArray.js", "testTypedArray.js"]; + if (control.status === "fail") + expect(() => runUpstream(control.source, "strict", includes)).toThrow(); + else expect(() => runUpstream(control.source, "strict", includes)).not.toThrow(); + const result = await runSource(control.source, { sanitize, includes }); + expect(result, JSON.stringify(result, null, 2)).toMatchObject({ status: control.status }); + }); + } + }); + const asyncControls = shardSelect( [ { diff --git a/tests/test262/README.md b/tests/test262/README.md index 46465645f..2f8a0ed3e 100644 --- a/tests/test262/README.md +++ b/tests/test262/README.md @@ -28,11 +28,11 @@ Pass the resulting directory as `--root` and use `--filter` to survey an API fam ## Execution profile and limits -The current profile is `static-adapted-v5`. It adapts strict scripts to standalone modules and sloppy scripts to CommonJS entries. Preparation supplies host assertions and a completion marker while leaving the upstream test body unchanged. Each program executes in a fresh native process. Conservative syntax checks reject known global-script dependencies, but cannot establish that every adaptation preserves the original script's semantics. Reports are local investigation artifacts, not published compatibility assessments. +The current profile is `static-adapted-v6`. It adapts strict scripts to standalone modules and sloppy scripts to CommonJS entries. Preparation supplies host assertions and a completion marker while leaving the upstream test body unchanged. Each program executes in a fresh native process. Conservative syntax checks reject known global-script dependencies, but cannot establish that every adaptation preserves the original script's semantics. Reports are local investigation artifacts, not published compatibility assessments. The unmodified upstream assertion helper currently reaches scriptc's JavaScript function-expando refusal. Test262 explicitly permits host implementations of harness functions in its [interpretation rules](https://github.com/tc39/test262/blob/7ab7fafa0003f73fc85c1b95d88094d33f7eb8bd/INTERPRETING.md#host-defined-functions). `harness.ts` supplies `assert`, `assert.sameValue`, `assert.notSameValue`, `assert.compareArray`, `assert.throws` for selected Error constructors, `$DONE`, and `Test262Error`. Scalar SameValue and scalar array-element comparisons use the statically compiled `node:assert/strict` implementation. Contract tests compare successful and failing assertions with the original Test262 helpers under Node, including NaN, signed zero, null/undefined, differing primitive types, and premature termination. -Reference-equality assertions terminate with a distinct harness-refusal result: conversion at a native function boundary can copy references, so this adapter cannot faithfully compare every object, array, or function identity. Tests cannot catch this refusal and accidentally pass. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons still refuse, while other property helpers and descriptor expressions remain excluded. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. +Reference-equality assertions terminate with a distinct harness-refusal result: conversion at a native function boundary can copy references, so this adapter cannot faithfully compare every object, array, or function identity. Tests cannot catch this refusal and accidentally pass. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons still refuse, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. Raw tests, module variants, agents, resolution negatives, and runtime negatives remain excluded. Parse negatives pass only when a source-matched syntax or vetted JavaScript early-error diagnostic appears; TypeScript type errors and harness-only failures never satisfy a negative expectation. `$262`, dynamic evaluation, observable script-global state, and other unsupported host requirements are excluded as well. The runtime and compiler continue to enforce their existing refusal boundaries for admitted source. diff --git a/tests/test262/run.ts b/tests/test262/run.ts index b798b3f21..2ad02100b 100644 --- a/tests/test262/run.ts +++ b/tests/test262/run.ts @@ -4,7 +4,7 @@ import { parseArgs } from "node:util"; import { shardSelect } from "../harness/shard.js"; import { runSource, type Outcome } from "./execute.js"; import { - assertThrowsSource, directory, exclusion, harnessSource, propertyHelperSource, matchesExpectation, matchesParseNegative, metadata, pin, sha256, snapshotDigest, summarize, + assertThrowsSource, directory, exclusion, harnessSource, propertyHelperSource, typedArrayHelperSource, matchesExpectation, matchesParseNegative, metadata, pin, sha256, snapshotDigest, summarize, testPaths, variants, vendorRoot, verifyVendor, } from "./support.mjs"; @@ -104,8 +104,8 @@ Other variants remain visible as exclusions.`); const report = { schema: "scriptc.test262.v1", revision: pin.commit, snapshotSha256: pin.snapshotSha256, - profile: "static-adapted-v5", - harnessSha256: sha256(harnessSource + assertThrowsSource + propertyHelperSource), + profile: "static-adapted-v6", + harnessSha256: sha256(harnessSource + assertThrowsSource + propertyHelperSource + typedArrayHelperSource), requestedBackend: backend, sanitize: process.env.SCRIPTC_SAN === "1", host: `${process.platform}-${process.arch}`, node: process.version, compilerVersion: JSON.parse(readFileSync(join(directory, "../../packages/compiler/package.json"), "utf8")).version, diff --git a/tests/test262/support.mjs b/tests/test262/support.mjs index e56612457..52739f91a 100644 --- a/tests/test262/support.mjs +++ b/tests/test262/support.mjs @@ -12,6 +12,8 @@ export const completion = "__scriptc_test262_complete__"; export const harnessSource = readFileSync(join(directory, "harness.ts"), "utf8"); export const assertThrowsSource = readFileSync(join(directory, "assert-throws.js"), "utf8"); export const propertyHelperSource = readFileSync(join(directory, "property-helper.js"), "utf8"); +export const typedArrayHelperSource = readFileSync(join(directory, "test-typed-array.js"), "utf8"); +const adaptedIncludes = new Set(["compareArray.js", "propertyHelper.js", "testTypedArray.js"]); export const expectations = JSON.parse(readFileSync(join(directory, "expectations.json"), "utf8")); export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); @@ -131,11 +133,12 @@ export function exclusion(source, meta, variant) { } if (variant !== "strict" && variant !== "sloppy") return `execution:${variant}`; if (meta.flags.some((flag) => flag.startsWith("CanBlock"))) return "host:agents"; - const unsupportedIncludes = meta.includes.filter((name) => name !== "compareArray.js" && name !== "propertyHelper.js"); + const unsupportedIncludes = meta.includes.filter((name) => !adaptedIncludes.has(name)); if (unsupportedIncludes.length) return `harness-includes:${unsupportedIncludes.join(",")}`; const sf = ts.createSourceFile("test.js", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); let reason; const propertyHelper = meta.includes.includes("propertyHelper.js"); + const typedArrayHelper = meta.includes.includes("testTypedArray.js"); const forbidden = new Set(["$262", "$DONE", "$DONOTEVALUATE", "globalThis", "eval", "Function", "print", "process", "require"]); if (variant === "sloppy") for (const name of ["module", "exports", "__dirname", "__filename"]) forbidden.add(name); if (meta.flags.includes("async")) forbidden.delete("$DONE"); @@ -170,6 +173,9 @@ export function exclusion(source, meta, variant) { reason = "harness:compareArray-surface"; } else if (ts.isIdentifier(node) && node.text === "Test262Error") { if (!ts.isNewExpression(node.parent) || node.parent.expression !== node) reason = "harness:Test262Error-surface"; + } else if (typedArrayHelper && ts.isIdentifier(node) && node.text === "TypedArray") { + // The adapted testTypedArray.js leaves out %TypedArray%. + reason = "harness:TypedArray-intrinsic"; } else if (propertyHelper && ts.isIdentifier(node)) { if (unsupportedPropertyHelpers.has(node.text)) reason = "harness:propertyHelper-surface"; else if (node.text === "verifyProperty") { @@ -188,7 +194,9 @@ export function exclusion(source, meta, variant) { export function prepare(source, asyncTest = false, marker = completion, variant = "strict", includes = []) { const names = asyncTest ? "assert, Test262Error, $DONE" : "assert, Test262Error"; const end = asyncTest ? "" : `;console.log(${JSON.stringify(marker)});\n`; - const helpers = includes.includes("propertyHelper.js") ? `${propertyHelperSource}\n` : ""; + const helpers = + (includes.includes("propertyHelper.js") ? `${propertyHelperSource}\n` : "") + + (includes.includes("testTypedArray.js") ? `${typedArrayHelperSource}\n` : ""); if (variant === "sloppy") return `const { ${names} } = require("./harness.ts");\n${helpers}${source}\n${end}`; return `"use strict";\nimport { ${names} } from "./harness.ts";\n${helpers}${source}\n${end}`; } diff --git a/tests/test262/test-typed-array.js b/tests/test262/test-typed-array.js new file mode 100644 index 000000000..f7ee73c84 --- /dev/null +++ b/tests/test262/test-typed-array.js @@ -0,0 +1,313 @@ +// Copyright (C) 2015 André Bargull. All rights reserved. +// This adaptation of Test262's testTypedArray.js is governed by the BSD license in vendor/LICENSE. +// It keeps the upstream constructor lists and argument factories. The host +// assert.js is replaced by harness.ts, so isPrimitive is defined here, and +// Test262Error is a class that needs `new`. Factories bind through a +// closure, and feature checks use typeof (and tolerate an ArrayBuffer +// without a reachable prototype, which then has no resizable or immutable +// factories). %TypedArray% (`TypedArray`) is left out: tests that name it +// are excluded. + +function isPrimitive(value) { + return !value || (typeof value !== "object" && typeof value !== "function"); +} + +var floatArrayConstructors = [ + Float64Array, + Float32Array +]; + +var nonClampedIntArrayConstructors = [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array +]; + +var intArrayConstructors = nonClampedIntArrayConstructors.concat([Uint8ClampedArray]); + +// Float16Array is a newer feature +// adding it to this list unconditionally would cause implementations lacking it to fail every test which uses it +if (typeof Float16Array !== "undefined") { + floatArrayConstructors.push(Float16Array); +} + +var bigIntArrayConstructors = []; +if (typeof BigInt64Array !== "undefined") { + bigIntArrayConstructors.push(BigInt64Array); +} +if (typeof BigUint64Array !== "undefined") { + bigIntArrayConstructors.push(BigUint64Array); +} + +var typedArrayConstructors = floatArrayConstructors.concat(intArrayConstructors); + +var allTypedArrayConstructors = typedArrayConstructors.concat(bigIntArrayConstructors); + +function makePassthrough(TA, primitiveOrIterable) { + return primitiveOrIterable; +} + +function makeArray(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable); + // Only values between 0 and 2**53 - 1 inclusive can get mapped into TA contents. + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return Array.from({ length: n }, function() { return "0"; }); + } + return Array.from(primitiveOrIterable); +} + +function makeArrayLike(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + var obj = { length: arr.length }; + for (var i = 0; i < obj.length; i++) obj[i] = arr[i]; + return obj; +} + +var makeIterable; +if (typeof Symbol !== "undefined") { + makeIterable = function makeIterable(TA, primitiveOrIterable) { + var src = makeArray(TA, primitiveOrIterable); + if (isPrimitive(src)) return src; + var obj = {}; + obj[Symbol.iterator] = function() { return src[Symbol.iterator](); }; + return obj; + }; +} + +function makeArrayBuffer(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + return new TA(arr).buffer; +} + +var makeResizableArrayBuffer, makeGrownArrayBuffer, makeShrunkArrayBuffer, makeImmutableArrayBuffer; +if (typeof ArrayBuffer.prototype === "object" && typeof ArrayBuffer.prototype.resize === "function") { + var copyIntoArrayBuffer = function(destBuffer, srcBuffer) { + var destView = new Uint8Array(destBuffer); + var srcView = new Uint8Array(srcBuffer); + for (var i = 0; i < srcView.length; i++) destView[i] = srcView[i]; + return destBuffer; + }; + + makeResizableArrayBuffer = function makeResizableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return new ArrayBuffer(n, { maxByteLength: n * 2 }); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var resizable = new ArrayBuffer(byteLength, { maxByteLength: byteLength * 2 }); + return copyIntoArrayBuffer(resizable, fixed); + }; + + makeGrownArrayBuffer = function makeGrownArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var grown = new ArrayBuffer(Math.floor(n / 2), { maxByteLength: n }); + grown.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var grown = new ArrayBuffer(Math.floor(byteLength / 2), { maxByteLength: byteLength }); + grown.resize(byteLength); + return copyIntoArrayBuffer(grown, fixed); + }; + + makeShrunkArrayBuffer = function makeShrunkArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var shrunk = new ArrayBuffer(n * 2, { maxByteLength: n * 2 }); + shrunk.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var shrunk = new ArrayBuffer(byteLength * 2, { maxByteLength: byteLength * 2 }); + copyIntoArrayBuffer(shrunk, fixed); + shrunk.resize(byteLength); + return shrunk; + }; +} +if (typeof ArrayBuffer.prototype === "object" && typeof ArrayBuffer.prototype.transferToImmutable === "function") { + makeImmutableArrayBuffer = function makeImmutableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return (new ArrayBuffer(n)).transferToImmutable(); + } + var mutable = makeArrayBuffer(TA, primitiveOrIterable); + return mutable.transferToImmutable(); + }; +} + +var typedArrayCtorArgFactories = [makePassthrough, makeArray, makeArrayLike]; +if (makeIterable) typedArrayCtorArgFactories.push(makeIterable); +typedArrayCtorArgFactories.push(makeArrayBuffer); +if (makeResizableArrayBuffer) typedArrayCtorArgFactories.push(makeResizableArrayBuffer); +if (makeGrownArrayBuffer) typedArrayCtorArgFactories.push(makeGrownArrayBuffer); +if (makeShrunkArrayBuffer) typedArrayCtorArgFactories.push(makeShrunkArrayBuffer); +if (makeImmutableArrayBuffer) typedArrayCtorArgFactories.push(makeImmutableArrayBuffer); + + +function ctorArgFactoryMatchesSome(argFactory, features) { + for (var i = 0; i < features.length; ++i) { + switch (features[i]) { + case "passthrough": + if (argFactory === makePassthrough) return true; + break; + case "arraylike": + if (argFactory === makeArray || argFactory === makeArrayLike) return true; + break; + case "iterable": + if (argFactory === makeIterable) return true; + break; + case "arraybuffer": + if ( + argFactory === makeArrayBuffer || + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer || + argFactory === makeImmutableArrayBuffer + ) { + return true; + } + break; + case "resizable": + if ( + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer + ) { + return true; + } + break; + case "immutable": + if (argFactory === makeImmutableArrayBuffer) return true; + break; + default: + throw new Test262Error("unknown feature: " + features[i]); + } + } + return false; +} + + +function bindArgFactory(argFactory, constructor) { + return function(primitiveOrIterable) { + return argFactory(constructor, primitiveOrIterable); + }; +} + +function testWithAllTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || allTypedArrayConstructors; + var ctorArgFactories = typedArrayCtorArgFactories; + if (includeArgFactories) { + ctorArgFactories = []; + for (var i = 0; i < typedArrayCtorArgFactories.length; ++i) { + if (ctorArgFactoryMatchesSome(typedArrayCtorArgFactories[i], includeArgFactories)) { + ctorArgFactories.push(typedArrayCtorArgFactories[i]); + } + } + } + if (excludeArgFactories) { + ctorArgFactories = ctorArgFactories.slice(); + for (var i = ctorArgFactories.length - 1; i >= 0; --i) { + if (ctorArgFactoryMatchesSome(ctorArgFactories[i], excludeArgFactories)) { + ctorArgFactories.splice(i, 1); + } + } + } + if (ctorArgFactories.length === 0) { + throw new Test262Error("no arg factories match include " + includeArgFactories + " and exclude " + excludeArgFactories); + } + for (var k = 0; k < ctorArgFactories.length; ++k) { + var argFactory = ctorArgFactories[k]; + for (var i = 0; i < ctors.length; ++i) { + var constructor = ctors[i]; + var boundArgFactory = bindArgFactory(argFactory, constructor); + try { + f(constructor, boundArgFactory); + } catch (e) { + e.message += " (Testing with " + constructor.name + " and " + argFactory.name + ".)"; + throw e; + } + } + } +} + +function testWithTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || typedArrayConstructors; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +function testWithBigIntTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || [BigInt64Array, BigUint64Array]; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +var nonAtomicsFriendlyTypedArrayConstructors = floatArrayConstructors.concat([Uint8ClampedArray]); +function testWithNonAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + nonAtomicsFriendlyTypedArrayConstructors, + includeArgFactories, + excludeArgFactories + ); +} + +function testWithAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array, + ], + includeArgFactories, + excludeArgFactories + ); +} + +function testTypedArrayConversions(byteConversionValues, fn) { + var values = byteConversionValues.values; + var expected = byteConversionValues.expected; + + testWithTypedArrayConstructors(function(TA) { + var name = TA.name.slice(0, -5); + + return values.forEach(function(value, index) { + var exp = expected[name][index]; + var initial = 0; + if (exp === 0) { + initial = 1; + } + fn(TA, value, exp, initial); + }); + }, null, ["passthrough"]); +} + +function isFloatTypedArrayConstructor(arg) { + return floatArrayConstructors.indexOf(arg) !== -1; +} + +function floatTypedArrayConstructorPrecision(FA) { + if (typeof Float16Array !== "undefined" && FA === Float16Array) { + return "half"; + } else if (FA === Float32Array) { + return "single"; + } else if (FA === Float64Array) { + return "double"; + } else { + throw new Error("Malformed test - floatTypedArrayConstructorPrecision called with non-float TypedArray"); + } +} diff --git a/tests/test262/upstream.json b/tests/test262/upstream.json index 1dc97ba20..025576e72 100644 --- a/tests/test262/upstream.json +++ b/tests/test262/upstream.json @@ -169,6 +169,12 @@ "test/built-ins/String/prototype/trim/15.5.4.20-2-47.js", "test/built-ins/String/prototype/trim/15.5.4.20-2-50.js", "test/built-ins/String/prototype/trim/15.5.4.20-2-6.js", + "test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js", + "test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js", + "test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js", + "test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js", + "test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js", + "test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js", "test/language/arguments-object/func-decl-args-trailing-comma-null.js", "test/language/arguments-object/func-expr-args-trailing-comma-undefined.js", "test/language/expressions/addition/S11.6.1_A4_T1.js", @@ -214,6 +220,7 @@ "harness/assert.js": "206e274ca325eb8a652e3911c3fbd090e2480d11ed7579dc17a5d17a2360ed48", "harness/propertyHelper.js": "08bc29114d804c4f24f906bac950ee4dc9c93c659d93258e7b44a54283ba9bb8", "harness/sta.js": "1930c54af79455c484799f43e9a28e2b2f15c40d0917c9941ca54e26db243f35", + "harness/testTypedArray.js": "51eb50e53e6215f31f14d64d249bea2d08ad943c77835f63ba52ec694de2189a", "test/annexB/language/function-code/function-redeclaration-block.js": "8d4a3f259dabceeee101959aac117a19f5bfe9e374e1178b45f45a1142195d9e", "test/annexB/language/function-code/if-decl-else-decl-b-func-existing-fn-no-init.js": "ccd95922fd6c784012f7299445f0859db04812232a2266dd90070b026244bbf5", "test/built-ins/Array/prototype/at/returns-item-relative-index.js": "71f53cf86f1387101b19b6ae9939d47ced34497d3cee12f954eef454f912ba88", @@ -377,6 +384,12 @@ "test/built-ins/String/prototype/trim/15.5.4.20-2-47.js": "3e1a745af465177a96a29601e56299afc044060dbc2ac3061a59032be451cfde", "test/built-ins/String/prototype/trim/15.5.4.20-2-50.js": "38b84997faa99e422408917eeac7f3575bd6064b429ea390a9d041fd77fce532", "test/built-ins/String/prototype/trim/15.5.4.20-2-6.js": "3d5857f24117f65f07a70edd0d254dfa07f593c76effed94500847ccf8564c06", + "test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js": "04cd6e31c8223319bbc6f5e5630ee476708a3897ead70e9cec34d40fb3ebd062", + "test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js": "51fd475317d19c863a85c26f1ffa2070b10abf123b49a3243bb3829218b0b7a9", + "test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js": "96741e885f333125077884cb59c23c297fad61a4834941a5ac38720227094d7d", + "test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js": "ea340740e8fb3227b19fff023508c38d3bd6b2ff45f7a623d1e4460fad9b32c1", + "test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js": "19d2ebe0d3db51eb9f978b9b2462bb38ff5cf4702f32e3163d8be46e851d0f8f", + "test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js": "711cf2873e49c9d48b3246f8dd067380ac902d286907b620735f5f4dbd36c943", "test/language/arguments-object/func-decl-args-trailing-comma-null.js": "80aecea8edd0d7bac1ddcfcd6cb97dc9a513b25a9283bb9cedd21fd77dd64d50", "test/language/arguments-object/func-expr-args-trailing-comma-undefined.js": "cf39515a9031db4c81db231a157516bdc97617c392e4ef28890b2ae74777a99e", "test/language/expressions/addition/S11.6.1_A4_T1.js": "12928eb319dcf0cc785e399580effb0b7495709296dce9c0a58e8408616a959f", diff --git a/tests/test262/vendor/harness/testTypedArray.js b/tests/test262/vendor/harness/testTypedArray.js new file mode 100644 index 000000000..a6418c2f7 --- /dev/null +++ b/tests/test262/vendor/harness/testTypedArray.js @@ -0,0 +1,431 @@ +// Copyright (C) 2015 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Collection of functions used to assert the correctness of TypedArray objects. +defines: + - floatArrayConstructors + - nonClampedIntArrayConstructors + - intArrayConstructors + - typedArrayConstructors + - TypedArray + - testWithAllTypedArrayConstructors + - testWithTypedArrayConstructors + - testWithBigIntTypedArrayConstructors + - nonAtomicsFriendlyTypedArrayConstructors + - testWithAtomicsFriendlyTypedArrayConstructors + - testWithNonAtomicsFriendlyTypedArrayConstructors + - testTypedArrayConversions +---*/ + +var floatArrayConstructors = [ + Float64Array, + Float32Array +]; + +var nonClampedIntArrayConstructors = [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array +]; + +var intArrayConstructors = nonClampedIntArrayConstructors.concat([Uint8ClampedArray]); + +// Float16Array is a newer feature +// adding it to this list unconditionally would cause implementations lacking it to fail every test which uses it +if (typeof Float16Array !== "undefined") { + floatArrayConstructors.push(Float16Array); +} + +var bigIntArrayConstructors = []; +if (typeof BigInt64Array !== "undefined") { + bigIntArrayConstructors.push(BigInt64Array); +} +if (typeof BigUint64Array !== "undefined") { + bigIntArrayConstructors.push(BigUint64Array); +} + +/** + * Array containing every non-bigint typed array constructor. + */ +var typedArrayConstructors = floatArrayConstructors.concat(intArrayConstructors); + +/** + * Array containing every typed array constructor, including those with bigint values. + */ +var allTypedArrayConstructors = typedArrayConstructors.concat(bigIntArrayConstructors); + +/** + * The %TypedArray% intrinsic constructor function. + */ +var TypedArray = Object.getPrototypeOf(Int8Array); + +function makePassthrough(TA, primitiveOrIterable) { + return primitiveOrIterable; +} + +function makeArray(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable); + // Only values between 0 and 2**53 - 1 inclusive can get mapped into TA contents. + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return Array.from({ length: n }, function() { return "0"; }); + } + return Array.from(primitiveOrIterable); +} + +function makeArrayLike(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + var obj = { length: arr.length }; + for (var i = 0; i < obj.length; i++) obj[i] = arr[i]; + return obj; +} + +var makeIterable; +if (typeof Symbol !== "undefined" && Symbol.iterator) { + makeIterable = function makeIterable(TA, primitiveOrIterable) { + var src = makeArray(TA, primitiveOrIterable); + if (isPrimitive(src)) return src; + var obj = {}; + obj[Symbol.iterator] = function() { return src[Symbol.iterator](); }; + return obj; + }; +} + +function makeArrayBuffer(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + return new TA(arr).buffer; +} + +var makeResizableArrayBuffer, makeGrownArrayBuffer, makeShrunkArrayBuffer, makeImmutableArrayBuffer; +if (ArrayBuffer.prototype.resize) { + var copyIntoArrayBuffer = function(destBuffer, srcBuffer) { + var destView = new Uint8Array(destBuffer); + var srcView = new Uint8Array(srcBuffer); + for (var i = 0; i < srcView.length; i++) destView[i] = srcView[i]; + return destBuffer; + }; + + makeResizableArrayBuffer = function makeResizableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return new ArrayBuffer(n, { maxByteLength: n * 2 }); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var resizable = new ArrayBuffer(byteLength, { maxByteLength: byteLength * 2 }); + return copyIntoArrayBuffer(resizable, fixed); + }; + + makeGrownArrayBuffer = function makeGrownArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var grown = new ArrayBuffer(Math.floor(n / 2), { maxByteLength: n }); + grown.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var grown = new ArrayBuffer(Math.floor(byteLength / 2), { maxByteLength: byteLength }); + grown.resize(byteLength); + return copyIntoArrayBuffer(grown, fixed); + }; + + makeShrunkArrayBuffer = function makeShrunkArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var shrunk = new ArrayBuffer(n * 2, { maxByteLength: n * 2 }); + shrunk.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var shrunk = new ArrayBuffer(byteLength * 2, { maxByteLength: byteLength * 2 }); + copyIntoArrayBuffer(shrunk, fixed); + shrunk.resize(byteLength); + return shrunk; + }; +} +if (ArrayBuffer.prototype.transferToImmutable) { + makeImmutableArrayBuffer = function makeImmutableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return (new ArrayBuffer(n)).transferToImmutable(); + } + var mutable = makeArrayBuffer(TA, primitiveOrIterable); + return mutable.transferToImmutable(); + }; +} + +var typedArrayCtorArgFactories = [makePassthrough, makeArray, makeArrayLike]; +if (makeIterable) typedArrayCtorArgFactories.push(makeIterable); +typedArrayCtorArgFactories.push(makeArrayBuffer); +if (makeResizableArrayBuffer) typedArrayCtorArgFactories.push(makeResizableArrayBuffer); +if (makeGrownArrayBuffer) typedArrayCtorArgFactories.push(makeGrownArrayBuffer); +if (makeShrunkArrayBuffer) typedArrayCtorArgFactories.push(makeShrunkArrayBuffer); +if (makeImmutableArrayBuffer) typedArrayCtorArgFactories.push(makeImmutableArrayBuffer); + +/** + * @typedef {"passthrough" | "arraylike" | "iterable" | "arraybuffer" | "resizable" | "immutable"} typedArrayArgFactoryFeature + */ + +/** + * A predicate for testing whether a TypedArray argument factory from this file + * matches any of the provided features. + * + * @param {Function} argFactory + * @param {typedArrayArgFactoryFeature[]} features + * @returns {boolean} + */ +function ctorArgFactoryMatchesSome(argFactory, features) { + for (var i = 0; i < features.length; ++i) { + switch (features[i]) { + case "passthrough": + if (argFactory === makePassthrough) return true; + break; + case "arraylike": + if (argFactory === makeArray || argFactory === makeArrayLike) return true; + break; + case "iterable": + if (argFactory === makeIterable) return true; + break; + case "arraybuffer": + if ( + argFactory === makeArrayBuffer || + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer || + argFactory === makeImmutableArrayBuffer + ) { + return true; + } + break; + case "resizable": + if ( + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer + ) { + return true; + } + break; + case "immutable": + if (argFactory === makeImmutableArrayBuffer) return true; + break; + default: + throw Test262Error("unknown feature: " + features[i]); + } + } + return false; +} + +/** + * Callback for testing a typed array constructor. + * + * @callback typedArrayConstructorCallback + * @param {Function} TypedArrayConstructor the constructor object to test with + * @param {Function} [TypedArrayConstructorArgFactory] a function for making + * a TypedArrayConstructor argument from a primitive (usually a number) or + * iterable (usually an array) + */ + +/** + * Calls the provided function with (typedArrayCtor, typedArrayCtorArgFactory) + * pairs, where typedArrayCtor is Uint8Array/Int8Array/BigInt64Array/etc. and + * typedArrayCtorArgFactory is a function for mapping a primitive (usually a + * number) or iterable (usually an array) into a value suitable as the first + * argument of typedArrayCtor (an Array, arraylike, iterable, or ArrayBuffer). + * + * @param {typedArrayConstructorCallback} f - the function to call + * @param {Array} [constructors] - an explicit list of TypedArray constructors + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithAllTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || allTypedArrayConstructors; + var ctorArgFactories = typedArrayCtorArgFactories; + if (includeArgFactories) { + ctorArgFactories = []; + for (var i = 0; i < typedArrayCtorArgFactories.length; ++i) { + if (ctorArgFactoryMatchesSome(typedArrayCtorArgFactories[i], includeArgFactories)) { + ctorArgFactories.push(typedArrayCtorArgFactories[i]); + } + } + } + if (excludeArgFactories) { + ctorArgFactories = ctorArgFactories.slice(); + for (var i = ctorArgFactories.length - 1; i >= 0; --i) { + if (ctorArgFactoryMatchesSome(ctorArgFactories[i], excludeArgFactories)) { + ctorArgFactories.splice(i, 1); + } + } + } + if (ctorArgFactories.length === 0) { + throw Test262Error("no arg factories match include " + includeArgFactories + " and exclude " + excludeArgFactories); + } + for (var k = 0; k < ctorArgFactories.length; ++k) { + var argFactory = ctorArgFactories[k]; + for (var i = 0; i < ctors.length; ++i) { + var constructor = ctors[i]; + var boundArgFactory = argFactory.bind(undefined, constructor); + try { + f(constructor, boundArgFactory); + } catch (e) { + e.message += " (Testing with " + constructor.name + " and " + argFactory.name + ".)"; + throw e; + } + } + } +} + +/** + * Calls the provided function with (typedArrayCtor, typedArrayCtorArgFactory) + * pairs, where typedArrayCtor is Uint8Array/Int8Array/BigInt64Array/etc. and + * typedArrayCtorArgFactory is a function for mapping a primitive (usually a + * number) or iterable (usually an array) into a value suitable as the first + * argument of typedArrayCtor (an Array, arraylike, iterable, or ArrayBuffer). + * + * typedArrayCtor will not be BigInt64Array or BigUint64Array unless one or both + * of those are explicitly provided. + * + * @param {typedArrayConstructorCallback} f - the function to call + * @param {Array} [constructors] - an explicit list of TypedArray constructors + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || typedArrayConstructors; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +/** + * Calls the provided function for every BigInt typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call + * @param {Array} [constructors] - an explicit list of TypedArray constructors + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithBigIntTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || [BigInt64Array, BigUint64Array]; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +var nonAtomicsFriendlyTypedArrayConstructors = floatArrayConstructors.concat([Uint8ClampedArray]); +/** + * Calls the provided function for every non-"Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithNonAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + nonAtomicsFriendlyTypedArrayConstructors, + includeArgFactories, + excludeArgFactories + ); +} + +/** + * Calls the provided function for every "Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array, + ], + includeArgFactories, + excludeArgFactories + ); +} + +/** + * Helper for conversion operations on TypedArrays, the expected values + * properties are indexed in order to match the respective value for each + * TypedArray constructor + * @param {Function} fn - the function to call for each constructor and value. + * will be called with the constructor, value, expected + * value, and a initial value that can be used to avoid + * a false positive with an equivalent expected value. + */ +function testTypedArrayConversions(byteConversionValues, fn) { + var values = byteConversionValues.values; + var expected = byteConversionValues.expected; + + testWithTypedArrayConstructors(function(TA) { + var name = TA.name.slice(0, -5); + + return values.forEach(function(value, index) { + var exp = expected[name][index]; + var initial = 0; + if (exp === 0) { + initial = 1; + } + fn(TA, value, exp, initial); + }); + }, null, ["passthrough"]); +} + +/** + * Checks if the given argument is one of the float-based TypedArray constructors. + * + * @param {constructor} ctor - the value to check + * @returns {boolean} + */ +function isFloatTypedArrayConstructor(arg) { + return floatArrayConstructors.indexOf(arg) !== -1; +} + +/** + * Determines the precision of the given float-based TypedArray constructor. + * + * @param {constructor} ctor - the value to check + * @returns {string} "half", "single", or "double" for Float16Array, Float32Array, and Float64Array respectively. + */ +function floatTypedArrayConstructorPrecision(FA) { + if (typeof Float16Array !== "undefined" && FA === Float16Array) { + return "half"; + } else if (FA === Float32Array) { + return "single"; + } else if (FA === Float64Array) { + return "double"; + } else { + throw new Error("Malformed test - floatTypedArrayConstructorPrecision called with non-float TypedArray"); + } +} diff --git a/tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js b/tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js new file mode 100644 index 000000000..cbca8c3ec --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js @@ -0,0 +1,31 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-get-%typedarray%.prototype.byteoffset +description: > + Return value from [[ByteOffset]] internal slot +info: | + 22.2.3.3 get %TypedArray%.prototype.byteOffset + + ... + 6. Let offset be the value of O's [[ByteOffset]] internal slot. + 7. Return size. +includes: [testTypedArray.js] +features: [TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var ta1 = new TA(); + assert.sameValue(ta1.byteOffset, 0, "Regular typedArray"); + + var offset = 4 * TA.BYTES_PER_ELEMENT; + + var buffer1 = makeCtorArg(8); + var ta2 = new TA(buffer1, offset); + assert.sameValue(ta2.byteOffset, offset, "TA(buffer, offset)"); + + var buffer2 = makeCtorArg(8); + var sample = new TA(buffer2, offset); + var ta3 = new TA(sample); + assert.sameValue(ta3.byteOffset, 0, "TA(typedArray)"); +}, null, ["arraybuffer"]); diff --git a/tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js b/tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js new file mode 100644 index 000000000..1e54741d3 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js @@ -0,0 +1,24 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-%typedarray%.prototype.subarray +description: Return abrupt from ToInteger(begin), begin is symbol +info: | + 22.2.3.27 %TypedArray%.prototype.subarray( begin , end ) + + ... + 7. Let relativeBegin be ? ToInteger(begin). + ... +includes: [testTypedArray.js] +features: [Symbol, TypedArray] +---*/ + +var s = Symbol("1"); + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var sample = new TA(makeCtorArg(0)); + + assert.throws(TypeError, function() { + sample.subarray(s); + }); +}); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js new file mode 100644 index 000000000..b4bf00149 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js @@ -0,0 +1,23 @@ +// Copyright (C) 2015 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: pending +description: > + TypedArray's [[ByteOffset]] internal slot is always a positive number, test with negative zero. +info: | + %TypedArray% ( buffer [ , byteOffset [ , length ] ] ) + + ... + 6. Let offset be ? ToInteger(byteOffset). + 7. If offset < 0, throw a RangeError exception. + 8. If offset is -0, let offset be +0. + ... +includes: [testTypedArray.js] +features: [SharedArrayBuffer, TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TAConstructor) { + var typedArray = new TAConstructor(new SharedArrayBuffer(8), -0); + assert.sameValue(typedArray.byteOffset, +0); +}, null, ["passthrough"]); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js new file mode 100644 index 000000000..c04f79f76 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js @@ -0,0 +1,27 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-typedarray-length +description: > + Throws a RangeError if length is a Infinity value +info: | + 22.2.4.2 TypedArray ( length ) + + This description applies only if the TypedArray function is called with at + least one argument and the Type of the first argument is not Object. + + ... + 4. Let numberLength be ? ToNumber(length). + 5. Let elementLength be ToLength(numberLength). + 6. If SameValueZero(numberLength, elementLength) is false, throw a RangeError + exception. + ... +includes: [testTypedArray.js] +features: [TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA) { + assert.throws(RangeError, function() { + new TA(Infinity); + }); +}, null, ["passthrough"]); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js new file mode 100644 index 000000000..e96b6d1d4 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js @@ -0,0 +1,27 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-typedarray-typedarray +description: > + Throws a TypeError if NewTarget is undefined. +info: | + 22.2.4.3 TypedArray ( typedArray ) + + This description applies only if the TypedArray function is called with at + least one argument and the Type of the first argument is Object and that + object has a [[TypedArrayName]] internal slot. + + ... + 2. If NewTarget is undefined, throw a TypeError exception. + ... +includes: [testTypedArray.js] +features: [TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var typedArray = new TA(makeCtorArg(4)); + + assert.throws(TypeError, function() { + TA(typedArray); + }); +}); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js new file mode 100644 index 000000000..898cf1abc --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js @@ -0,0 +1,39 @@ +// Copyright (C) 2021 Alexey Shvayka. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-integer-indexed-exotic-objects-defineownproperty-p-desc +description: > + Throws TypeError for valid index & non-configurable descriptor. +info: | + [[DefineOwnProperty]] ( P, Desc ) + + [...] + 3. If Type(P) is String, then + a. Let numericIndex be ! CanonicalNumericIndexString(P). + b. If numericIndex is not undefined, then + [...] + ii. If Desc has a [[Configurable]] field and if Desc.[[Configurable]] is false, return false. +includes: [testTypedArray.js] +features: [align-detached-buffer-semantics-with-web-reality, TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var sample = new TA(makeCtorArg([0])); + + assert.throws(TypeError, function() { + Object.defineProperty(sample, "0", { + configurable: false, + }); + }, "partial descriptor"); + + assert.throws(TypeError, function() { + Object.defineProperty(sample, "0", { + value: 42, + writable: true, + enumerable: true, + configurable: false, + }); + }, "complete descriptor"); + + assert.sameValue(sample[0], 0, "side effect check"); +}, null, ["passthrough"]); From 46b00f69c97629ba4df012f30583a3d5262e1b66 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:30:29 -0500 Subject: [PATCH 26/45] Give glibc's per-thread heaps huge pages Blocks above 512 bytes come from glibc, which serves each worker thread from a 64 MiB heap of its own and makes it accessible with mprotect a page-rounded step at a time. Every 2 MiB range of a heap is first touched while part of it is still inaccessible, so the heaps never get transparent huge pages: on a 16-checker type check of VS Code they hold about 0.9 GB on small pages, close to half of the process's page faults. Once a worker thread has allocated 16 MiB of large blocks, the heap of each large block it gets is made accessible whole and advised MADV_HUGEPAGE, once per heap. glibc's calloc still clears only what lies below its own high-water mark, so no page is touched early. glibc's mmap and trim thresholds are set to 4 MiB and 8 MiB, so that the largest blocks get mappings of their own and a heap's free top goes back to the system: with huge pages, a heap otherwise keeps resident memory it never writes. The brk heap policy's own thresholds take precedence once it switches on. The policy is off unless THP is in madvise mode and malloc is glibc's, and when the glibc.malloc.hugetlb tunable is set or the system forbids overcommit. A heap is advised only when /proc/self/maps shows anonymous mappings covering exactly its range. Library archives leave their embedder's heaps alone. --- packages/runtime/src/scr_alloc.c | 101 ++++++++++++++++++++++++++ packages/runtime/src/scr_alloc.test.c | 53 ++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 3a36f672e..f3d6c0893 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -567,7 +567,108 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * These loads run only on blocks too large for the small allocator, next to * a glibc malloc call: plain loads on x86-64, LDAR (or LDAPR) on arm64. */ +#if defined(SCR_WORKERS) && !defined(SCR_LIB) +/* glibc's per-thread heaps (worker executables; library archives run on + * their embedder's threads and leave its heaps alone). glibc serves a thread's + * blocks from a heap of its own: a 64 MiB mapping, aligned to its size, + * that it makes accessible with mprotect as it grows, a page-rounded step + * at a time, so every 2 MiB range is first touched while part of it is + * still inaccessible and gets small pages. Once a thread has allocated + * SCR_SA_THREAD_HUGE_AFTER bytes of large blocks (threads with few keep + * small pages), the heap of each large block it gets is made accessible + * whole and advised huge pages, once per heap; glibc's calloc still clears + * only what lies below its own high-water mark, so this touches nothing. + * glibc's mmap and trim thresholds are fixed at SCR_SA_THREAD_MMAP_THRESHOLD + * and SCR_SA_THREAD_TRIM_THRESHOLD: lower than the maxima glibc's adaptive + * thresholds reach, so that blocks of 4 MiB and more get mappings of their + * own and a heap's free top goes back once it passes 8 MiB, while huge + * pages keep resident memory that a heap would otherwise leave untouched. + * The brk heap policy below sets its own (higher) values when the main heap + * grows past SCR_SA_HEAP_HUGE_AFTER, and those take precedence. The + * policy is off unless THP is in madvise mode, when the glibc.malloc.hugetlb + * tunable is set (glibc then advises its heaps itself, or sizes them by + * hugetlbfs pages), when the system forbids overcommit (making a heap + * accessible would commit all of it), and when malloc is not glibc's (a + * sanitizer or a preloaded allocator: blocks have no glibc chunk header). + * A heap is only advised when /proc/self/maps shows the anonymous mappings + * of exactly its range. */ +#define SCR_SA_GLIBC_HEAP ((uintptr_t)64 << 20) /* glibc's heap size and alignment */ +#define SCR_SA_THREAD_HUGE_AFTER ((size_t)16 << 20) +#define SCR_SA_THREAD_MMAP_THRESHOLD (4 << 20) +#define SCR_SA_THREAD_TRIM_THRESHOLD (8 << 20) +static _Thread_local size_t scr_sa_thread_large_bytes; +static _Thread_local uintptr_t scr_sa_thread_heap; /* the heap this thread advised last */ +static _Atomic(int) scr_sa_thread_heaps; /* 0 undecided, 1 advised, 2 off */ +extern void *__libc_malloc(size_t); + +/* Whether [heap, heap + SCR_SA_GLIBC_HEAP) is covered exactly by anonymous + * mappings, as a glibc heap is (its accessible part and the rest). */ +static bool scr_sa_glibc_heap_mapped(uintptr_t heap) { + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) return false; + char line[512]; + uintptr_t next = heap, end = heap + SCR_SA_GLIBC_HEAP; + while (next < end && fgets(line, sizeof line, f)) { + unsigned long lo, hi, inode; + char path[2] = {0}; + if (sscanf(line, "%lx-%lx %*s %*s %*s %lu %1s", &lo, &hi, &inode, path) < 3) continue; + if (hi <= next) continue; + if (lo != next || inode != 0 || path[0]) break; + next = hi; + } + fclose(f); + return next == end; +} + +static void scr_sa_thread_heap_note(void *p, size_t n) { + if (scr_sa_thread_large_bytes < SCR_SA_THREAD_HUGE_AFTER) { + scr_sa_thread_large_bytes += n; + return; + } + int mode = atomic_load_explicit(&scr_sa_thread_heaps, memory_order_relaxed); + if (mode == 2) return; + if (mode == 0) { + int undecided = 0; + const char *tunables = getenv("GLIBC_TUNABLES"); + FILE *f = fopen("/proc/sys/vm/overcommit_memory", "r"); + int overcommit = f ? fgetc(f) : '0'; + if (f) fclose(f); + /* Loaded through volatile: the compiler takes two declared functions + * to have different addresses. */ + void *(*volatile used)(size_t) = malloc, *(*volatile own)(size_t) = __libc_malloc; + bool glibc_malloc = used == own; + mode = glibc_malloc && scr_sa_thp_mode() == 'm' && !(tunables && strstr(tunables, "glibc.malloc.hugetlb")) && + overcommit != '2' + ? 1 + : 2; + if (atomic_compare_exchange_strong(&scr_sa_thread_heaps, &undecided, mode) && mode == 1 && + atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed) != 1) { + (void)mallopt(M_MMAP_THRESHOLD, SCR_SA_THREAD_MMAP_THRESHOLD); + (void)mallopt(M_TRIM_THRESHOLD, SCR_SA_THREAD_TRIM_THRESHOLD); + /* The brk heap policy marks itself on before it sets its values: if it + * did so meanwhile, its values are set again. */ + if (atomic_load(&scr_sa_heap_state) == 1) { + (void)mallopt(M_MMAP_THRESHOLD, SCR_SA_HEAP_MMAP_THRESHOLD); + (void)mallopt(M_TRIM_THRESHOLD, SCR_SA_HEAP_TRIM_THRESHOLD); + } + } + mode = atomic_load_explicit(&scr_sa_thread_heaps, memory_order_relaxed); + if (mode != 1) return; + } + /* glibc's chunk header: NON_MAIN_ARENA (4) set, IS_MMAPPED (2) clear. */ + if ((((const size_t *)p)[-1] & 6) != 4) return; + uintptr_t heap = (uintptr_t)p & ~(SCR_SA_GLIBC_HEAP - 1); + if (heap == scr_sa_thread_heap) return; + scr_sa_thread_heap = heap; + if (scr_sa_glibc_heap_mapped(heap) && mprotect((void *)heap, SCR_SA_GLIBC_HEAP, PROT_READ | PROT_WRITE) == 0) + (void)madvise((void *)heap, SCR_SA_GLIBC_HEAP, MADV_HUGEPAGE); +} +#endif + static void scr_sa_large_note(void *p, size_t n) { +#if defined(SCR_WORKERS) && !defined(SCR_LIB) + if (p) scr_sa_thread_heap_note(p, n); +#endif int state = atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed); if (state == 2 || !p) return; uintptr_t top = (uintptr_t)sbrk(0); diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index e20c0733b..6ccb1495b 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -343,6 +343,56 @@ static void *reclaim_arena(void *arg) { return NULL; } +#if defined(__linux__) && defined(__GLIBC__) +/* A thread whose large blocks pass 16 MiB gets them from a glibc heap of + * its own, which is then advised huge pages where THP is in madvise mode. */ +enum { HEAP_BLOCKS = 320, HEAP_BLOCK = 64 << 10 }; +static void *heap_blocks[HEAP_BLOCKS]; +static char heap_flags[256]; +static void *thread_heap_allocate(void *arg) { + (void)arg; + for (unsigned i = 0; i < HEAP_BLOCKS; i++) { + heap_blocks[i] = scr_mem_alloc(HEAP_BLOCK); + assert(heap_blocks[i] && !owned(heap_blocks[i])); + ((unsigned char *)heap_blocks[i])[0] = (unsigned char)i; + } + vm_flags(heap_blocks[HEAP_BLOCKS - 1], heap_flags, sizeof heap_flags); + for (unsigned i = 0; i < HEAP_BLOCKS; i++) { + assert(((unsigned char *)heap_blocks[i])[0] == (unsigned char)i); + scr_mem_free(heap_blocks[i]); + } + scr_sa_thread_park(); + return NULL; +} + +static void check_thread_heap_huge_pages(void) { + pthread_t t; + assert(pthread_create(&t, NULL, thread_heap_allocate, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + char mode[96] = {0}; + FILE *thp = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r"); + if (thp) { + size_t n = fread(mode, 1, sizeof mode - 1, thp); + mode[n] = '\0'; + fclose(thp); + } + FILE *f = fopen("/proc/sys/vm/overcommit_memory", "r"); + int overcommit = f ? fgetc(f) : '0'; + if (f) fclose(f); + /* ThreadSanitizer replaces malloc, which turns the policy off. */ + bool glibc_malloc = true; +#if defined(__SANITIZE_THREAD__) + glibc_malloc = false; +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) + glibc_malloc = false; +#endif +#endif + if (glibc_malloc && strstr(mode, "[madvise]") && !getenv("GLIBC_TUNABLES") && overcommit != '2') + assert(strstr(heap_flags, " hg")); +} +#endif + static void check_threads(void) { void *mine = scr_mem_alloc(48); assert(owned(mine)); @@ -1047,6 +1097,9 @@ int main(void) { setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); check_threads(); +#if defined(__linux__) && defined(__GLIBC__) + check_thread_heap_huge_pages(); +#endif #ifdef __linux__ check_trim(); check_release_frontiers(); From 19d6736b01b65bfaa75f3be92f1bcd49c0acc74d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:04:56 -0500 Subject: [PATCH 27/45] Look up exports whose names start with "__" by their escaped key TypeScript keys a symbol table entry whose name starts with `__` with an extra `_` (`__x` is stored as `___x`; `symbolName` removes it). Native ESM link checking looked an import up by its own spelling, so a program that imports such an export compiled to Node's startup SyntaxError: // dep.ts export const __x = 1; // main.ts import { __x } from "./dep.ts"; // The requested module './dep.ts' // does not provide an export named '__x' `escapedSymbolName`, the inverse of `symbolName`, gives a name's table key. Link checking, external type lookups, CommonJS export lookups and the `this` check of ES module exports now look names up through it. `Object.keys` over a module namespace skipped every key starting with `__` to drop TypeScript's internal names, so it also dropped these exports; it now skips only internal names and lists exports by their own names. Closes #1582. Co-authored-by: thekevinbrown <1531750+thekevinbrown@users.noreply.github.com> --- .../frontend/lowering/cjs-exports-object.ts | 2 +- .../src/frontend/lowering/lower-calls.ts | 6 ++++-- .../compiler/src/frontend/lowering/lowerer.ts | 4 ++-- packages/compiler/src/frontend/program.ts | 2 +- .../src/frontend/ts7/semantic-model.test.ts | 6 ++++++ .../src/frontend/ts7/semantic-model.ts | 6 ++++++ .../src/frontend/ts7/semantic-types.ts | 2 +- .../corpus/7306-dunder-exports/main.ts.json | 10 ++++++++++ tests/corpus/7306-dunder-exports/dep.ts | 19 ++++++++++++++++++ tests/corpus/7306-dunder-exports/facade.ts | 2 ++ tests/corpus/7306-dunder-exports/main.ts | 20 +++++++++++++++++++ tests/corpus/7306-dunder-exports/star.ts | 4 ++++ tests/corpus/7306-dunder-exports/table.cjs | 5 +++++ .../7306-dunder-exports/main.ts.json | 1 + 14 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json create mode 100644 tests/corpus/7306-dunder-exports/dep.ts create mode 100644 tests/corpus/7306-dunder-exports/facade.ts create mode 100644 tests/corpus/7306-dunder-exports/main.ts create mode 100644 tests/corpus/7306-dunder-exports/star.ts create mode 100644 tests/corpus/7306-dunder-exports/table.cjs create mode 100644 tests/harness/corpus-costs/7306-dunder-exports/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index ae8cce4ca..5e5f24e32 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -381,7 +381,7 @@ function esModuleExportIgnoresThis( depth: number, ): boolean { const moduleSym = lowerer.checker.getSymbolAtLocation(sf); - const exported = moduleSym?.getExports().get(name as ts.__String); + const exported = moduleSym?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 31276263d..5366ca9d0 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -8777,11 +8777,13 @@ function lowerObjectStaticCall( const moduleSymbol = source ? lowerer.checker.getSymbolAtLocation(source) : undefined; const names: string[] = []; moduleSymbol?.getExports().forEach((symbol, key) => { + // Internal names (`__export`) start with `__`; an exported name that + // does is keyed with an extra `_` (escapedSymbolName). const name = String(key); - if (name.startsWith("__") || name === "export=") return; + if ((name.startsWith("__") && !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); + if (target.flags & ts.SymbolFlags.Value) names.push(ts.symbolName(symbol)); }); names.sort(); const loc = locOf(call); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fff4a043b..7faba254f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4219,7 +4219,7 @@ export class Lowerer { // exact mappings, while star exports resolve to the mapped declaration // owner through externalTypeSpecifiersByFile. const moduleSymbol = this.checker.getSymbolAtLocation(sf); - const exported = moduleSymbol?.getExports().get(exportName as ts.__String); + const exported = moduleSymbol?.getExports().get(ts.escapedSymbolName(exportName)); return this.externalTypeSpecifierOfSymbol(exported, seenSymbols, seenExports); } @@ -4362,7 +4362,7 @@ export class Lowerer { cjsModuleExportSymbol(sf: ts.SourceFile, name: string): ts.Symbol | undefined { const moduleSym = this.checker.getSymbolAtLocation(sf); const exports = moduleSym?.getExports(); - const direct = exports?.get(name as ts.__String); + const direct = exports?.get(ts.escapedSymbolName(name)); if (direct) return direct; // tsgo represents `module.exports = { ... }` as one `export=` symbol // and does not duplicate the object literal's properties into the diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 6d8ffe4c0..e92251554 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -6437,7 +6437,7 @@ function analyzeEsmNamedImportLinks( const nextSeen = new Set(seenModules); nextSeen.add(dep); const module = checker.getSymbolAtLocation(dep); - const exported = module?.getExports().get(name as ts.__String); + const exported = module?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { let resolved = exported; const seenSymbols = new Set(); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.test.ts b/packages/compiler/src/frontend/ts7/semantic-model.test.ts index 9d0f61a4e..bbcd53d8b 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.test.ts @@ -28,6 +28,7 @@ import { SemanticSnapshot, SemanticType, symbolNameOf, + escapedSymbolName, } from "./semantic-model.js"; import type { __String } from "./ast-types.js"; import { @@ -313,6 +314,11 @@ test("symbol tables keep escaped keys, display names, canonical context and cach const exports = parent.getExports(); expect(symbolNameOf(members.get("___x" as __String))).toBe("__x"); expect(symbolNameOf(exports.get("__x" as __String))).toBe("__x"); + // A name that starts with `__` is keyed with an extra `_`. + expect(symbolNameOf(members.get(escapedSymbolName("__x")))).toBe("__x"); + expect(escapedSymbolName("x")).toBe("x"); + expect(escapedSymbolName("_x")).toBe("_x"); + expect(escapedSymbolName("___y")).toBe("____y"); expect(parent.getMembers()).toBe(members); expect(parent.getExports()).toBe(exports); expect(parent.getParent()).toBe(first.symbol(symbol(2))); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index d3d5fe61a..405f4775b 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -454,6 +454,12 @@ export function symbolName(symbol: SemanticSymbol): string { return name.startsWith("___") ? name.slice(1) : name; } +/** The key of the name `name` in a symbol table (`getExports()`, `getMembers()`): the name, with + * an extra `_` when it starts with `__`. The inverse of `symbolName`. */ +export function escapedSymbolName(name: string): __String { + return (name.startsWith("__") ? `_${name}` : name) as __String; +} + /** `symbolName(symbol)`, or undefined without a symbol. */ export function symbolNameOf(symbol: SemanticSymbol | null | undefined): string | undefined { return symbol === undefined || symbol === null ? undefined : symbolName(symbol); diff --git a/packages/compiler/src/frontend/ts7/semantic-types.ts b/packages/compiler/src/frontend/ts7/semantic-types.ts index ea43e3885..5a597ae4e 100644 --- a/packages/compiler/src/frontend/ts7/semantic-types.ts +++ b/packages/compiler/src/frontend/ts7/semantic-types.ts @@ -30,7 +30,7 @@ export type { SemanticSignature as Signature, SemanticSymbol as Symbol, } from "./semantic-model.js"; -export { symbolName, symbolNameOf } from "./semantic-model.js"; +export { escapedSymbolName, symbolName, symbolNameOf } from "./semantic-model.js"; export type { SemanticIndexInfo as IndexInfo, SemanticTypePredicate as TypePredicate, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json new file mode 100644 index 000000000..fd14c0dfe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/7306-dunder-exports/dep.ts", + "/tests/corpus/7306-dunder-exports/star.ts", + "/tests/corpus/7306-dunder-exports/facade.ts", + "/tests/corpus/7306-dunder-exports/table.cjs", + "/tests/corpus/7306-dunder-exports/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7306-dunder-exports/dep.ts b/tests/corpus/7306-dunder-exports/dep.ts new file mode 100644 index 000000000..6d54eb365 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/dep.ts @@ -0,0 +1,19 @@ +export const __x = 1; +export const ___y = "three"; +export let __count = 0; +export function __bump(): number { + __count++; + return __count; +} +export class __Point { + a: number; + b: number; + constructor(a: number, b: number) { + this.a = a; + this.b = b; + } + sum(): number { + return this.a + this.b; + } +} +export const plain = "plain"; diff --git a/tests/corpus/7306-dunder-exports/facade.ts b/tests/corpus/7306-dunder-exports/facade.ts new file mode 100644 index 000000000..89ab6eb32 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/facade.ts @@ -0,0 +1,2 @@ +export { __x, __x as __alias, ___y as renamed } from "./dep.ts"; +export * from "./star.ts"; diff --git a/tests/corpus/7306-dunder-exports/main.ts b/tests/corpus/7306-dunder-exports/main.ts new file mode 100644 index 000000000..415bc4a77 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/main.ts @@ -0,0 +1,20 @@ +// Exports whose names start with "__": named imports of a const, a function, +// a class and a live let, a name with three underscores, aliased re-exports, +// names reached through `export *`, namespace member reads and keys, and +// CommonJS exports read through require. +import { __x, ___y, __count, __bump, __Point, plain } from "./dep.ts"; +import { __alias, renamed, __starred, __twice } from "./facade.ts"; +import * as facade from "./facade.ts"; +import * as depNs from "./dep.ts"; +import { createRequire } from "node:module"; + +console.log(__x, ___y, plain); +console.log(__count, __bump(), __bump(), __count); +console.log(new __Point(2, 3).sum()); +console.log(__alias, renamed, __starred, __twice(21)); +console.log(facade.__x, facade.__starred, facade.__twice(2)); +console.log(Object.keys(depNs).join(",")); + +const require = createRequire(import.meta.url); +const table = require("./table.cjs"); +console.log(table.__a, table.__hello("dunder"), table.b); diff --git a/tests/corpus/7306-dunder-exports/star.ts b/tests/corpus/7306-dunder-exports/star.ts new file mode 100644 index 000000000..6ca21d73d --- /dev/null +++ b/tests/corpus/7306-dunder-exports/star.ts @@ -0,0 +1,4 @@ +export const __starred = "from export *"; +export function __twice(n: number): number { + return n * 2; +} diff --git a/tests/corpus/7306-dunder-exports/table.cjs b/tests/corpus/7306-dunder-exports/table.cjs new file mode 100644 index 000000000..e775925ff --- /dev/null +++ b/tests/corpus/7306-dunder-exports/table.cjs @@ -0,0 +1,5 @@ +exports.__a = 10; +exports.__hello = function (name) { + return "hello " + name; +}; +exports.b = 20; diff --git a/tests/harness/corpus-costs/7306-dunder-exports/main.ts.json b/tests/harness/corpus-costs/7306-dunder-exports/main.ts.json new file mode 100644 index 000000000..0477ef4d5 --- /dev/null +++ b/tests/harness/corpus-costs/7306-dunder-exports/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3600 } From e75174aeea3d01e31ce4d8f2b3f2f2adeba679bd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:13:03 -0500 Subject: [PATCH 28/45] Compile string-key destructuring of any values and cast-guarded Buffer aliases Co-authored-by: patlux <4481570+patlux@users.noreply.github.com> Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com> --- .../lowering/expressions/identifiers.ts | 11 ++++ .../frontend/lowering/expressions/typeof.ts | 6 ++ .../frontend/lowering/expressions/updates.ts | 4 ++ .../src/frontend/lowering/lower-exprs.ts | 4 ++ .../src/frontend/lowering/lower-stmts.ts | 9 ++- .../compiler/src/frontend/lowering/lowerer.ts | 4 ++ .../src/frontend/lowering/surfaces.ts | 32 ++++++++++ .../7309-globalthis-buffer-aliases.ts.json | 6 ++ .../7310-any-destructure-string-keys.ts.json | 6 ++ .../any-destructure-unspelled-keys.ts.json | 6 ++ .../globalthis-buffer-alias-values.ts.json | 6 ++ .../corpus/7309-globalthis-buffer-aliases.ts | 31 ++++++++++ .../7310-any-destructure-string-keys.ts | 60 +++++++++++++++++++ .../any-destructure-unspelled-keys.ts | 12 ++++ .../globalthis-buffer-alias-values.ts | 12 ++++ .../any-destructure-unspelled-keys.ts.txt | 27 +++++++++ .../globalthis-buffer-alias-values.ts.txt | 17 ++++++ .../7309-globalthis-buffer-aliases.ts.json | 1 + .../7310-any-destructure-string-keys.ts.json | 1 + 19 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json create mode 100644 tests/corpus/7309-globalthis-buffer-aliases.ts create mode 100644 tests/corpus/7310-any-destructure-string-keys.ts create mode 100644 tests/diagnostics/any-destructure-unspelled-keys.ts create mode 100644 tests/diagnostics/globalthis-buffer-alias-values.ts create mode 100644 tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt create mode 100644 tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt create mode 100644 tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json create mode 100644 tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ef0d8a2d1..b1116647a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -744,6 +744,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { + // An alias through a cast of globalThis carries the cast's static type, + // which the identity token below does not have. + const aliasSymbol = identifierReadSymbol(lowerer, expr); + if (aliasSymbol && lowerer.castGlobalAliases.has(aliasSymbol)) { + lowerer.unsupported( + "SC1090", + expr, + "the Buffer constructor read through a cast of globalThis as a value", + "test it in a condition, read its typeof, or call its members", + ); + } return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; } if (lowerer.isStdlibGlobal(expr, "fetch")) { diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index cc3ac06fb..fbbc068b9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -88,6 +88,12 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (builtinClassRefOf(lowerer, expr.expression) !== null) { return { kind: "strLit", value: "function", type: STRING, loc }; } + // The Buffer constructor and its aliases (`const B = globalThis.Buffer`, + // `(globalThis as { Buffer?: B }).Buffer`): a value read of an alias is an + // identity token, so its typeof is decided here. + if (stdlibGlobalNameOf(lowerer, expr.expression) === "Buffer") { + return { kind: "strLit", value: "function", type: STRING, loc }; + } if (ts.isPropertyAccessExpression(expr.expression)) { const member = expr.expression; const prototype = member.expression; diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 04ca23a96..7cc77a2db 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -191,6 +191,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio operatorUnsupported(lowerer, expr, "+", [raw.type]); } case ts.SyntaxKind.ExclamationToken: { + // `!runtimeBuffer` over an alias of the Buffer global: Node always + // defines Buffer (the condition twin is lowerCondition's). + if (lowerer.isStdlibGlobal(expr.operand, "Buffer")) + return { kind: "boolLit", value: false, type: BOOL, loc }; // `!x` is ToBoolean-then-negate: f64/string operands go through toBool. const operand = lowerer.ensureBool( deferredSlotOr( diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 2bde5f2aa..c1c5ef8c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1087,6 +1087,10 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.isStdlibGlobal(e.expression, "Object") ) return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) }; + // `if (runtimeBuffer)` over an alias of the Buffer global (a + // capability probe): Node always defines Buffer. + if (lowerer.isStdlibGlobal(e, "Buffer")) + return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) }; if (ts.isPropertyAccessExpression(e)) { const presence = lowerPromiseThenPresence(lowerer, e, { kind: "boolLit", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e66d0e855..e11f79c25 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -8749,13 +8749,20 @@ function destrSpellingOf(rhs: ts.Expression): string | null { return base === null ? null : `${base}.${e.name.text}`; } // `table[key]` and `list[0]`; V8 prints a numeric key in its canonical - // number form (`o[1.50]` reads "o[1.5]"). + // number form (`o[1.50]` reads "o[1.5]"), and a string key as a property + // name with its text decoded (`o["a.b"]` and `o[\`a.b\`]` read "o.a.b"). + // The message is a C string, so a key holding NUL keeps the refusal. if (ts.isElementAccessExpression(e) && !e.questionDotToken) { const base = destrSpellingOf(e.expression); const key = e.argumentExpression; if (base === null) return null; if (ts.isIdentifier(key)) return `${base}[${key.text}]`; if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`; + if ( + (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) && + !key.text.includes("\0") + ) + return `${base}.${key.text}`; return null; } if (ts.isCallExpression(e) && !e.questionDotToken) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fff4a043b..441b313f0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2937,6 +2937,10 @@ export class Lowerer { * checks resolve through this map (stdlibGlobalNameOf), declarations * emit nothing. */ readonly stdlibGlobalAliases = new Map(); + /** The stdlibGlobalAliases bound through a cast of the global object + * (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the + * cast's, so their value reads are refused (identifiers.ts). */ + readonly castGlobalAliases = new Set(); /** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`), * lifted lazily as module-level functions and interned per accessor * declaration: member reads call the getter (lower-exprs). */ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44564f2f1..ed615b276 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -6,6 +6,20 @@ function unparenthesized(e: ts.Expression): ts.Expression { while (ts.isParenthesizedExpression(e)) e = e.expression; return e; } + +/** `e` without parentheses and type-only wrappers (as, satisfies, type + * assertions, non-null assertions), which change no runtime value. */ +function withoutTypeCasts(e: ts.Expression): ts.Expression { + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isTypeAssertion(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + return e; +} import { UNSUPPORTED } from "../../diagnostics/diagnostic.js"; import { BOOL, @@ -3440,6 +3454,17 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + // A cast of the global object (`(globalThis as { Buffer?: B }).Buffer`, + // `(globalThis as any).Buffer`) changes only its static type: the + // member is the global object's own. + const uncast = withoutTypeCasts(expr.expression); + if (uncast !== unparenthesized(expr.expression)) { + if (!ts.isIdentifier(uncast) || stdlibGlobalNameOf(lowerer, uncast) !== "globalThis") + return null; + const member = lowerer.checker.getPropertyOfType(lowerer.typeOf(uncast), expr.name.text); + if (!member || !lowerer.isStdlibSymbol(member)) return null; + return ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); + } if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; // Through a detection chain the checker types the receiver as the // union of every arm; the selected arm (the global object) owns the @@ -3572,6 +3597,13 @@ export function stdlibGlobalAliasDecl( const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (!symbol) return false; lowerer.stdlibGlobalAliases.set(symbol, name); + const access = init !== undefined ? unparenthesized(init) : undefined; + if ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + withoutTypeCasts(access.expression) !== unparenthesized(access.expression) + ) + lowerer.castGlobalAliases.add(symbol); return true; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json new file mode 100644 index 000000000..94d458e11 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7309-globalthis-buffer-aliases.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json new file mode 100644 index 000000000..45a9f62ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7310-any-destructure-string-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json new file mode 100644 index 000000000..f38d71f47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/any-destructure-unspelled-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json new file mode 100644 index 000000000..4f6fe0c8c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/globalthis-buffer-alias-values.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7309-globalthis-buffer-aliases.ts b/tests/corpus/7309-globalthis-buffer-aliases.ts new file mode 100644 index 000000000..39d896463 --- /dev/null +++ b/tests/corpus/7309-globalthis-buffer-aliases.ts @@ -0,0 +1,31 @@ +// Aliases of the global Buffer constructor read through a cast of +// globalThis, the capability-probe spelling of code that also runs where +// Buffer is absent: typed and `as any` casts, `if`, ternary, typeof and `!` +// guards, member calls through the alias, and typeof of the alias. +type RuntimeBuffer = { + byteLength(value: string, encoding: "utf8"): number; + from(value: string, encoding: "utf8"): Uint8Array; + isBuffer(value: unknown): boolean; +}; + +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +const anyBuffer = (globalThis as any).Buffer; +const text = "héllo wörld"; + +if (runtimeBuffer) { + console.log("typed", runtimeBuffer.byteLength(text, "utf8")); +} else { + console.log("typed: no Buffer"); +} +if (typeof anyBuffer === "function") { + console.log("any", anyBuffer.byteLength(text, "utf8")); +} +const bytes = runtimeBuffer ? runtimeBuffer.from(text, "utf8") : undefined; +console.log("from", bytes ? bytes.length : -1); +console.log("isBuffer", runtimeBuffer ? runtimeBuffer.isBuffer(bytes) : false); +console.log("typeof", typeof runtimeBuffer, typeof anyBuffer, !runtimeBuffer); + +function utf8Length(value: string): number { + return runtimeBuffer ? runtimeBuffer.byteLength(value, "utf8") : value.length; +} +console.log("function", utf8Length("✓✓"), utf8Length("abc")); diff --git a/tests/corpus/7310-any-destructure-string-keys.ts b/tests/corpus/7310-any-destructure-string-keys.ts new file mode 100644 index 000000000..4ab24ec77 --- /dev/null +++ b/tests/corpus/7310-any-destructure-string-keys.ts @@ -0,0 +1,60 @@ +// Destructuring an `any` source read through a string-literal key +// (`const { value } = events["a.b"]`): reads, renames, defaults, rest, a +// nested pattern, the assignment form, a template-literal key, chained +// string keys, and V8's TypeError for undefined and null sources, whose +// message spells the key as a property name with its text decoded. +const events: any = { + "a.b": { value: 1, label: "first", extra: true, nested: { deep: "d" } }, + "x-y": { value: 2 }, + outer: { "in.ner": { value: 3 } }, + "nul": null, +}; + +const { value } = events["a.b"]; +const { value: renamed, missing = "fallback", ...rest } = events["a.b"]; +const { + nested: { deep }, +} = events["a.b"]; +console.log(value, renamed, missing, JSON.stringify(rest), deep); + +let assigned: unknown; +({ value: assigned } = events[`x-y`]); +const { value: chained } = events["outer"]["in.ner"]; +console.log(assigned, chained); + +function attempt(label: string, f: () => unknown): void { + try { + console.log(label, f()); + } catch (e) { + console.log(label, e instanceof TypeError, (e as Error).message); + } +} +attempt("missing", () => { + const { value } = events["no.such"]; + return value; +}); +attempt("null", () => { + const { value } = events["nul"]; + return value; +}); +attempt("quotes", () => { + const { value } = events["it's \"quoted\""]; + return value; +}); +attempt("escapes", () => { + const { value } = events["tab\there\nnext"]; + return value; +}); +attempt("accents", () => { + const { value } = events["été ✓"]; + return value; +}); +attempt("template", () => { + const { value } = events[`plain`]; + return value; +}); +attempt("assignment", () => { + let target: unknown; + ({ value: target } = events["gone"]); + return target; +}); diff --git a/tests/diagnostics/any-destructure-unspelled-keys.ts b/tests/diagnostics/any-destructure-unspelled-keys.ts new file mode 100644 index 000000000..6680de854 --- /dev/null +++ b/tests/diagnostics/any-destructure-unspelled-keys.ts @@ -0,0 +1,12 @@ +// Destructuring an `any` source keeps SC1031 where the source has no +// compiled spelling for V8's TypeError: a key computed by an expression, an +// optional element read, a template key with a substitution, and a string key +// holding NUL (the message is a C string). String-literal keys compile +// (tests/corpus/7310-any-destructure-string-keys.ts). +const events: any = { ab: { value: 1 } }; +const k = "a"; +const { value: computed } = events["a" + "b"]; +const { value: optional } = events?.["ab"]; +const { value: template } = events[`${k}b`]; +const { value: nul } = events["a\0b"]; +console.log(computed, optional, template, nul); diff --git a/tests/diagnostics/globalthis-buffer-alias-values.ts b/tests/diagnostics/globalthis-buffer-alias-values.ts new file mode 100644 index 000000000..97b49f920 --- /dev/null +++ b/tests/diagnostics/globalthis-buffer-alias-values.ts @@ -0,0 +1,12 @@ +// An alias of the Buffer global read through a cast of globalThis compiles +// in conditions, typeof and member calls +// (tests/corpus/7309-globalthis-buffer-aliases.ts). Its other value uses +// keep a named SC1090 refusal: the alias has the cast's static type. +type RuntimeBuffer = { from(value: string, encoding: "utf8"): Uint8Array }; +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +function keep(value: unknown): unknown { + return value; +} +const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); +keep(runtimeBuffer); +keep(bytes); diff --git a/tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt b/tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt new file mode 100644 index 000000000..872b717cf --- /dev/null +++ b/tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt @@ -0,0 +1,27 @@ +any-destructure-unspelled-keys.ts:8:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 7 | const k = "a"; + 8 | const { value: computed } = events["a" + "b"]; + | ^~~~~~~~~~~~~~~~~ + 9 | const { value: optional } = events?.["ab"]; + +any-destructure-unspelled-keys.ts:9:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 8 | const { value: computed } = events["a" + "b"]; + 9 | const { value: optional } = events?.["ab"]; + | ^~~~~~~~~~~~~~ + 10 | const { value: template } = events[`${k}b`]; + +any-destructure-unspelled-keys.ts:10:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 9 | const { value: optional } = events?.["ab"]; + 10 | const { value: template } = events[`${k}b`]; + | ^~~~~~~~~~~~~~~ + 11 | const { value: nul } = events["a\0b"]; + +any-destructure-unspelled-keys.ts:11:24 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 10 | const { value: template } = events[`${k}b`]; + 11 | const { value: nul } = events["a\0b"]; + | ^~~~~~~~~~~~~~ + 12 | console.log(computed, optional, template, nul); \ No newline at end of file diff --git a/tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt b/tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt new file mode 100644 index 000000000..f55423db9 --- /dev/null +++ b/tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt @@ -0,0 +1,17 @@ +globalthis-buffer-alias-values.ts:10:15 - error SC1090: the Buffer constructor read through a cast of globalThis as a value is not supported yet + + 9 | } + 10 | const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); + | ^~~~~~~~~~~~~ + 11 | keep(runtimeBuffer); + + hint: test it in a condition, read its typeof, or call its members + +globalthis-buffer-alias-values.ts:11:6 - error SC1090: the Buffer constructor read through a cast of globalThis as a value is not supported yet + + 10 | const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); + 11 | keep(runtimeBuffer); + | ^~~~~~~~~~~~~ + 12 | keep(bytes); + + hint: test it in a condition, read its typeof, or call its members \ No newline at end of file diff --git a/tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json b/tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json new file mode 100644 index 000000000..f1c2cc30b --- /dev/null +++ b/tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 3400 } diff --git a/tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json b/tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json new file mode 100644 index 000000000..df39abc08 --- /dev/null +++ b/tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 3900 } From 97b64fae1cc66582b2e0ffe13008d817acfc4966 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:13:36 -0500 Subject: [PATCH 29/45] Turn the thread-heap huge-page policy off under memory tagging --- packages/runtime/src/scr_alloc.c | 26 +++++++++++++++++++++++--- packages/runtime/src/scr_alloc.test.c | 6 +++++- packages/runtime/src/scr_alloc.test.ts | 20 ++++++++++++++------ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index f3d6c0893..a27df378a 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -568,6 +568,9 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * a glibc malloc call: plain loads on x86-64, LDAR (or LDAPR) on arm64. */ #if defined(SCR_WORKERS) && !defined(SCR_LIB) +#if defined(__aarch64__) +#include +#endif /* glibc's per-thread heaps (worker executables; library archives run on * their embedder's threads and leave its heaps alone). glibc serves a thread's * blocks from a heap of its own: a 64 MiB mapping, aligned to its size, @@ -588,8 +591,11 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * policy is off unless THP is in madvise mode, when the glibc.malloc.hugetlb * tunable is set (glibc then advises its heaps itself, or sizes them by * hugetlbfs pages), when the system forbids overcommit (making a heap - * accessible would commit all of it), and when malloc is not glibc's (a - * sanitizer or a preloaded allocator: blocks have no glibc chunk header). + * accessible would commit all of it), when malloc is not glibc's (a + * sanitizer or a preloaded allocator: blocks have no glibc chunk header), + * and when memory tagging is on (arm64 MTE, the glibc.mem.tagging tunable): + * a chunk header then carries another tag than the block's pointer, and + * reading it through that pointer faults. * A heap is only advised when /proc/self/maps shows the anonymous mappings * of exactly its range. */ #define SCR_SA_GLIBC_HEAP ((uintptr_t)64 << 20) /* glibc's heap size and alignment */ @@ -601,6 +607,20 @@ static _Thread_local uintptr_t scr_sa_thread_heap; /* the heap this thread advis static _Atomic(int) scr_sa_thread_heaps; /* 0 undecided, 1 advised, 2 off */ extern void *__libc_malloc(size_t); +/* Whether glibc tags heap memory: the glibc.mem.tagging tunable is set to a + * nonzero value, or (arm64) the kernel checks tags for this process. */ +static bool scr_sa_mem_tagging(const char *tunables) { + const char *t = tunables ? strstr(tunables, "glibc.mem.tagging=") : NULL; + if (t && t[sizeof "glibc.mem.tagging=" - 1] != '0') return true; +#if defined(__aarch64__) + /* PR_GET_TAGGED_ADDR_CTRL (56); PR_MTE_TCF_MASK is bits 1-2. A kernel + * without the call returns -1: no tag checks. */ + long ctrl = prctl(56, 0, 0, 0, 0); + if (ctrl > 0 && (ctrl & (3L << 1))) return true; +#endif + return false; +} + /* Whether [heap, heap + SCR_SA_GLIBC_HEAP) is covered exactly by anonymous * mappings, as a glibc heap is (its accessible part and the rest). */ static bool scr_sa_glibc_heap_mapped(uintptr_t heap) { @@ -638,7 +658,7 @@ static void scr_sa_thread_heap_note(void *p, size_t n) { void *(*volatile used)(size_t) = malloc, *(*volatile own)(size_t) = __libc_malloc; bool glibc_malloc = used == own; mode = glibc_malloc && scr_sa_thp_mode() == 'm' && !(tunables && strstr(tunables, "glibc.malloc.hugetlb")) && - overcommit != '2' + overcommit != '2' && !scr_sa_mem_tagging(tunables) ? 1 : 2; if (atomic_compare_exchange_strong(&scr_sa_thread_heaps, &undecided, mode) && mode == 1 && diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index 6ccb1495b..dfdd9bf5c 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -388,7 +388,11 @@ static void check_thread_heap_huge_pages(void) { glibc_malloc = false; #endif #endif - if (glibc_malloc && strstr(mode, "[madvise]") && !getenv("GLIBC_TUNABLES") && overcommit != '2') + /* With glibc's memory tagging tunable set the heap is left alone: under + * arm64 MTE, reading its chunk headers through block pointers faults. */ + const char *tunables = getenv("GLIBC_TUNABLES"); + if (tunables && strstr(tunables, "glibc.mem.tagging=3")) assert(!strstr(heap_flags, " hg")); + else if (glibc_malloc && strstr(mode, "[madvise]") && !tunables && overcommit != '2') assert(strstr(heap_flags, " hg")); } #endif diff --git a/packages/runtime/src/scr_alloc.test.ts b/packages/runtime/src/scr_alloc.test.ts index 187ffce51..0ceda33cd 100644 --- a/packages/runtime/src/scr_alloc.test.ts +++ b/packages/runtime/src/scr_alloc.test.ts @@ -11,13 +11,20 @@ const exec = promisify(execFile); // UBSan-only build is the one that exercises it; the ASan build pins the // system-allocator bypass the sanitizer lane relies on. // Worker executables (-DSCR_WORKERS) give each script thread its own arena; -// the ThreadSanitizer build checks the cross-thread handoff for races. +// the ThreadSanitizer build checks the cross-thread handoff for races. The +// last build runs with glibc's memory tagging tunable set, which turns the +// thread-heap huge-page policy off. test.each([ - ["size-class", ["-fsanitize=undefined"]], - ["system", ["-fsanitize=address,undefined"]], - ["size-class workers", ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"]], - ["size-class workers", ["-fsanitize=thread", "-DSCR_WORKERS", "-pthread"]], -])("small-object allocator contracts hold (%s, %j)", async (mode, sanitize) => { + ["size-class", ["-fsanitize=undefined"], {}], + ["system", ["-fsanitize=address,undefined"], {}], + ["size-class workers", ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"], {}], + ["size-class workers", ["-fsanitize=thread", "-DSCR_WORKERS", "-pthread"], {}], + [ + "size-class workers", + ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"], + { GLIBC_TUNABLES: "glibc.mem.tagging=3" }, + ], +])("small-object allocator contracts hold (%s, %j, %j)", async (mode, sanitize, extraEnv) => { const scratch = await mkdtemp(join(tmpdir(), "scriptc-alloc-")); try { const binary = join(scratch, "alloc"); @@ -41,6 +48,7 @@ test.each([ ASAN_OPTIONS: "halt_on_error=1", TSAN_OPTIONS: "halt_on_error=1", UBSAN_OPTIONS: "halt_on_error=1", + ...extraEnv, }, }); expect(result.stdout).toBe(`allocator checks passed: ${mode}\n`); From fa80286f9b164b01921457f14983953f8d7f5845 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:21:21 -0500 Subject: [PATCH 30/45] Lower borrowed typed array set calls and keep method calls direct after dictionary stores Co-authored-by: ainslec <201870+ainslec@users.noreply.github.com> Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com> --- .../lowering/class-method-mutations.ts | 17 ++- .../src/frontend/lowering/containers/bytes.ts | 128 +++++++++++++++--- .../src/frontend/lowering/lower-calls.ts | 3 +- .../test/class-method-mutations.test.ts | 32 +++++ .../7940-typed-array-prototype-set.mjs.json | 6 + .../7941-typed-array-prototype-set.ts.json | 6 + ...7942-dictionary-store-method-calls.ts.json | 6 + .../corpus/7940-typed-array-prototype-set.mjs | 40 ++++++ .../corpus/7941-typed-array-prototype-set.ts | 21 +++ .../7942-dictionary-store-method-calls.ts | 45 ++++++ 10 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json create mode 100644 tests/corpus/7940-typed-array-prototype-set.mjs create mode 100644 tests/corpus/7941-typed-array-prototype-set.ts create mode 100644 tests/corpus/7942-dictionary-store-method-calls.ts diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 035c2c2a5..a07a4d3e1 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -125,11 +125,13 @@ export function collectClassMethodMutations( }; // Whether no function value can inhabit `type`. Primitives qualify, and so // do non-callable object types a function is not assignable to: one that - // requires a property functions lack, or a weak (all-optional) type that + // requires a property functions lack, a weak (all-optional) type that // shares no property with functions (TypeScript's weak-type rule rejects - // callable sources too). - const functionFree = (type: ts.Type): boolean => { - if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree); + // callable sources too), or a dictionary with no properties whose index + // signatures hold function-free values (functions have no index + // signature). `seen` stops recursive dictionary types. + const functionFree = (type: ts.Type, seen: Set = new Set()): boolean => { + if (type.isUnionType()) return ts.constituentTypes(type).every((t) => functionFree(t, seen)); if ( (type.flags & (ts.TypeFlags.StringLike | @@ -149,7 +151,12 @@ export function collectClassMethodMutations( ) return false; const properties = lowerer.checker.getPropertiesOfType(type); - if (properties.length === 0) return false; + if (properties.length === 0) { + if (seen.has(type)) return true; + seen.add(type); + const indexes = lowerer.checker.getIndexInfosOfType(type); + return indexes.length > 0 && indexes.every((index) => functionFree(index.type, seen)); + } const required = properties.filter( (property) => (property.flags & ts.SymbolFlags.Optional) === 0, ); diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 995cb177a..36e7ecd22 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,5 +1,5 @@ import { keyedHelperName } from "../stable-names.js"; -import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; +import { boolLit, dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; import { @@ -903,6 +903,116 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return lowerArrayBufferView(lowerer, args, "dv", locOf(expr)); } +/** `receiver.set(src, offset?)` on a lowered typed-array receiver. */ +function bytesSetIntrinsic( + lowerer: Lowerer, + receiver: IrExpr, + argNodes: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr { + const src = lowerer.lowerExpr(argNodes[0]!); + const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; + const args = [src.type.kind === "bytes" ? src : lowerer.coerceInto(argNodes[0]!, src, DYN)]; + if (argNodes.length === 2) + args.push( + lowerOptionalArgument(lowerer, argNodes[1]!, F64, { + kind: "numLit", + value: 0, + type: F64, + loc, + }), + ); + return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc }; +} + +/** `Uint8Array.prototype.set.call(target, src, offset?)`, through any typed + * array constructor's prototype (they share %TypedArray%.prototype.set). A + * typed-array target lowers like `target.set(src, offset)`. Any other + * target is checked at run time: a typed array calls its `set`, and + * anything else throws Node's TypeError "this is not a typed array.", + * after every argument is evaluated. */ +export function lowerBytesPrototypeSetCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if ( + call.questionDotToken || + access.questionDotToken || + access.name.text !== "call" || + !ts.isPropertyAccessExpression(access.expression) + ) + return null; + const method = access.expression; + const proto = method.expression; + if ( + method.questionDotToken || + method.name.text !== "set" || + !ts.isPropertyAccessExpression(proto) || + proto.questionDotToken || + proto.name.text !== "prototype" || + !ts.isIdentifier(proto.expression) || + !lowerer.isStdlibMember(method) + ) + return null; + const ctor = lowerer.resolveValueSymbol(proto.expression); + if (!ctor || !lowerer.isStdlibSymbol(ctor) || !own(BYTES_CTORS, ts.symbolName(ctor))) return null; + const argNodes = call.arguments.nodes; + if (argNodes.length < 2 || argNodes.length > 3 || argNodes.some(ts.isSpreadElement)) return null; + const loc = locOf(call); + const target = lowerer.lowerExpr(argNodes[0]!); + if (target.type.kind === "bytes") + return bytesSetIntrinsic(lowerer, target, argNodes.slice(1), loc); + const slot = (value: IrExpr): { decl: IrStmt; read: IrExpr } => { + const local = lowerer.declareHiddenLocal("%setArg", DYN); + return { + decl: { kind: "varDecl", localId: local.id, init: value, loc }, + read: varRef(local.id, DYN, loc), + }; + }; + const slots = [ + slot(lowerer.coerceInto(argNodes[0]!, target, DYN)), + ...argNodes.slice(1).map((arg) => slot(lowerer.lowerExprExpecting(arg, DYN))), + ]; + const isTypedArray: IrExpr = { + kind: "libCall", + fn: "util.typeIs", + args: [slots[0]!.read, { kind: "strLit", value: "isTypedArray", type: STRING, loc }], + type: BOOL, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + ...slots.map((s) => s.decl), + { + kind: "if", + cond: { kind: "unary", op: "!", operand: isTypedArray, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: nodeThrowExpr(1, "", "this is not a typed array.", VOID, loc), + loc, + }, + ], + else_: null, + loc, + }, + ], + result: { + kind: "dynInvoke", + recv: slots[0]!.read, + method: "set", + calleeName: ts.getTextOfNode(method), + args: slots.slice(1).map((s) => s.read), + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + /** Method calls on typed-array/Buffer receivers: slice (copy), subarray * (shared view), toReversed, copyWithin, set(src, * offset?), and the u8-only Buffer surface: toString(enc?) plus the @@ -1106,21 +1216,7 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const src = lowerer.lowerExpr(call.arguments.nodes[0]!); - const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; - const args = [ - src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN), - ]; - if (nArgs === 2) - args.push( - lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, { - kind: "numLit", - value: 0, - type: F64, - loc, - }), - ); - return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc }; + return bytesSetIntrinsic(lowerer, receiver, call.arguments.nodes, loc); } if (name === "toString") { // Buffer's toString(encoding?) — utf8 by default. A 0-arg toString diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 31276263d..c071d5a83 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -119,7 +119,7 @@ import { } from "./containers/indexed-objects.js"; import { lowerTupleReadMethodCall } from "./containers/array-methods.js"; import { tryFuseIndexRead } from "./array-values.js"; -import { lowerBytesStaticCall } from "./containers/bytes.js"; +import { lowerBytesPrototypeSetCall, lowerBytesStaticCall } from "./containers/bytes.js"; import { lowerRegexMethodCall, lowerStringIndexCall, @@ -1579,6 +1579,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerTestMethodCall(expr, expr.expression) ?? lowerer.lowerTimeoutMethodCall(expr, expr.expression) ?? lowerArrayPrototypeBorrowCall(lowerer, expr, expr.expression) ?? + lowerBytesPrototypeSetCall(lowerer, expr, expr.expression) ?? lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ?? lowerObjectPrototypeCall(lowerer, expr, expr.expression) ?? lowerStringPrototypeCall(lowerer, expr, expr.expression) ?? diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts index b508a3949..4871dda2b 100644 --- a/packages/compiler/test/class-method-mutations.test.ts +++ b/packages/compiler/test/class-method-mutations.test.ts @@ -135,3 +135,35 @@ test("a reflection call with a well-known symbol key keeps checked dispatch", as const callers = await callersOf(`${range}${write}`, "%Range.sym:iterator"); expect(callers.filter((name) => !name.startsWith("%method.value:"))).toEqual([]); }); + +const dictionaries = ` +class Counter { + n = 0; + add(): number { return ++this.n; } +} +interface Settings { groups: Record> } +interface Tree { [key: string]: Tree } +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +function grow(tree: Tree, name: string): void { + tree[name] = {}; +} +const settings: Settings = { groups: {} }; +addGroup(settings, "colours"); +grow({}, "a"); +const counter = new Counter(); +console.log(counter.add(), Object.keys(settings.groups)); +`; + +test("a store into a dictionary of dictionaries leaves method calls direct", async () => { + expect(await callersOf(dictionaries, "%dyn.class.properties")).toEqual([]); +}); + +test("a store into a dictionary whose values can be functions keeps checked dispatch", async () => { + const source = dictionaries.replace( + "Record>", + "Record | (() => number)>", + ); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json new file mode 100644 index 000000000..3c59407b3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7940-typed-array-prototype-set.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json new file mode 100644 index 000000000..8fbec6a90 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7941-typed-array-prototype-set.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json new file mode 100644 index 000000000..f04478490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7942-dictionary-store-method-calls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7940-typed-array-prototype-set.mjs b/tests/corpus/7940-typed-array-prototype-set.mjs new file mode 100644 index 000000000..4f1b2301d --- /dev/null +++ b/tests/corpus/7940-typed-array-prototype-set.mjs @@ -0,0 +1,40 @@ +// Uint8Array.prototype.set borrowed through .call on untyped receivers. +let cached = null; +function memory() { + if (cached === null) cached = new Uint8Array(4); + return cached; +} +function copy(target, source, offset) { + return Uint8Array.prototype.set.call(target, source, offset); +} +console.log(copy(memory(), new Uint8Array([3, 4]), 1)); +console.log(String(memory())); +copy(memory(), [9], undefined); +console.log(String(memory())); + +// Every typed array shares %TypedArray%.prototype.set. +const wide = new Int16Array(3); +copy(wide, [1, -2], 1); +console.log(String(wide)); +Float64Array.prototype.set.call(memory(), new Uint8Array([7]), 3); +console.log(String(memory())); + +const order = []; +const arg = (label, value) => { + order.push(label); + return value; +}; +for (const target of [{}, [0, 0], "ab", null, new DataView(new ArrayBuffer(2))]) { + order.length = 0; + try { + Uint8Array.prototype.set.call(arg("target", target), arg("source", [1]), arg("offset", 0)); + console.log("set"); + } catch (e) { + console.log(e instanceof TypeError, e.message, order.join(",")); + } +} +try { + copy(memory(), [1, 2], 3); +} catch (e) { + console.log(e instanceof RangeError, e.message); +} diff --git a/tests/corpus/7941-typed-array-prototype-set.ts b/tests/corpus/7941-typed-array-prototype-set.ts new file mode 100644 index 000000000..db880e38a --- /dev/null +++ b/tests/corpus/7941-typed-array-prototype-set.ts @@ -0,0 +1,21 @@ +// Uint8Array.prototype.set borrowed through .call on typed receivers. +const target = new Uint8Array(4); +Uint8Array.prototype.set.call(target, new Uint8Array([3, 4]), 1); +console.log(String(target)); +Uint8Array.prototype.set.call(target, [5]); +console.log(String(target)); + +const wide = new Int32Array(3); +Uint8Array.prototype.set.call(wide, new Uint8Array([200, 100]), 1); +console.log(String(wide)); + +function fill(view: Uint8Array, bytes: number[], at: number): void { + Uint8Array.prototype.set.call(view, bytes, at); +} +fill(target, [8, 9], 2); +console.log(String(target)); +try { + fill(target, [1, 2, 3], 2); +} catch (e) { + console.log(e instanceof RangeError, (e as Error).message); +} diff --git a/tests/corpus/7942-dictionary-store-method-calls.ts b/tests/corpus/7942-dictionary-store-method-calls.ts new file mode 100644 index 000000000..0056c26f6 --- /dev/null +++ b/tests/corpus/7942-dictionary-store-method-calls.ts @@ -0,0 +1,45 @@ +// Stores into dictionaries whose values cannot be functions leave class +// methods in place. +interface Data { + items: string[]; +} +class Store { + data: Data = { items: [] }; + get(): Data { + return this.data; + } +} +class Counter { + n = 0; + add(): number { + return ++this.n; + } +} +interface Settings { + groups: Record>; +} +interface Tree { + [key: string]: Tree; +} +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +function grow(tree: Tree, name: string): Tree { + tree[name] = {}; + return tree[name]!; +} +const settings: Settings = { groups: {} }; +addGroup(settings, "colours"); +addGroup(settings, "fonts"); +settings.groups["colours"]!["fg"] = "red"; +console.log(Object.keys(settings.groups).join(","), JSON.stringify(settings.groups)); +const tree: Tree = {}; +grow(grow(tree, "a"), "b"); +console.log(JSON.stringify(tree)); +const store = new Store(); +store.get().items.push("a"); +store.get().items.push("b"); +const counter = new Counter(); +let total = 0; +for (let i = 0; i < 1000; i++) total += counter.add(); +console.log(store.data.items.length, total, typeof counter.add); From 9eb14207a63e971c46a66b76c5a796d3702a7a92 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:53:36 -0500 Subject: [PATCH 31/45] Revert "Cache untyped property access in programs that declare classes (reland)" This reverts #2017 (adb8690f1), which made main CI fail. --- benchmarks/runtime/README.md | 1 - .../runtime/workloads/dynamic-properties.json | 6 - .../runtime/workloads/dynamic-properties.mjs | 56 ------ packages/compiler/src/ir/js-ic.test.ts | 188 ------------------ packages/compiler/src/ir/js-ic.ts | 92 +-------- packages/compiler/test/js-ic-emission.test.ts | 44 ---- packages/runtime/src/scr_js.h | 11 +- packages/runtime/src/scr_js_object.c | 32 +-- packages/runtime/src/scr_js_object.test.c | 26 +-- scripts/zero-cost-compare.mjs | 5 +- tests/harness/library-mode.test.ts | 20 -- tests/library-mode/js-ic-sessions/lib.ts | 6 - tests/library-mode/js-ic-sessions/probe.c | 25 --- .../library-mode/js-ic-sessions/profile.json | 14 -- tests/library-mode/js-ic-sessions/work.mjs | 26 --- 15 files changed, 31 insertions(+), 521 deletions(-) delete mode 100644 benchmarks/runtime/workloads/dynamic-properties.json delete mode 100644 benchmarks/runtime/workloads/dynamic-properties.mjs delete mode 100644 tests/library-mode/js-ic-sessions/lib.ts delete mode 100644 tests/library-mode/js-ic-sessions/probe.c delete mode 100644 tests/library-mode/js-ic-sessions/profile.json delete mode 100644 tests/library-mode/js-ic-sessions/work.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 04c90f7bb..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -13,7 +13,6 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | | `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | | `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | -| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | diff --git a/benchmarks/runtime/workloads/dynamic-properties.json b/benchmarks/runtime/workloads/dynamic-properties.json deleted file mode 100644 index 398981ef3..000000000 --- a/benchmarks/runtime/workloads/dynamic-properties.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "entry": "workloads/dynamic-properties.mjs", - "args": [ - "1000000" - ] -} diff --git a/benchmarks/runtime/workloads/dynamic-properties.mjs b/benchmarks/runtime/workloads/dynamic-properties.mjs deleted file mode 100644 index d0ee3802c..000000000 --- a/benchmarks/runtime/workloads/dynamic-properties.mjs +++ /dev/null @@ -1,56 +0,0 @@ -// Untyped JavaScript reading and writing properties of values it never -// declares types for: records built by object literals in two key orders, -// nested records, property updates, and instances of a class and its -// subclass read and written the same way. Argument: the number of -// iterations. -const rounds = Number(process.argv[2] ?? "1000000"); -class Cursor { - constructor(x, y) { - this.x = x; - this.y = y; - } - step(dx, dy) { - this.x += dx; - this.y += dy; - return this; - } -} -class Marker extends Cursor { - constructor(x, y, label) { - super(x, y); - this.label = label; - } -} -function makeItem(i) { - if (i % 3 === 0) - return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } }; - return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } }; -} -function cost(item) { - return item.price * item.qty + item.meta.weight; -} -function restock(item, round) { - item.qty = (item.qty + round) % 7; - item.meta.tag = (item.meta.tag + 1) % 7; -} -function distance(a, b) { - return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); -} -const items = []; -for (let i = 0; i < 1024; i++) items.push(makeItem(i)); -const cursors = []; -for (let i = 0; i < 64; i++) - cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i)); -const origin = { x: 0, y: 0 }; -let checksum = 0; -for (let r = 0; r < rounds; r++) { - const item = items[r & 1023]; - checksum += cost(item); - restock(item, r & 3); - checksum += item.meta.tag; - const cursor = cursors[r & 63]; - checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin); - if (cursor.x > 100 || cursor.x < -100) cursor.x = 0; - if (cursor.y > 100 || cursor.y < -100) cursor.y = 0; -} -console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label); diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index f5391b83e..f71a1e9db 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -126,140 +126,6 @@ test("property helpers whose class branches only test capsules are cache sites", expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); }); -test("bags converted between dyn and jsval are still the receiver", () => { - // A program that declares a class returns its bags as jsvals, and the - // helpers read them through js.toDyn. - const toDyn = (value: IrExpr): IrExpr => ({ - kind: "libCall", - fn: "js.toDyn", - args: [value], - type: DYN, - loc, - }); - const fromDyn = (value: IrExpr): IrExpr => ({ - kind: "libCall", - fn: "js.fromDyn", - args: [value], - type: { kind: "jsval" }, - loc, - }); - const jsvalBag = helper( - "%dyn.class.readPropertiesOrEmpty.js", - ["v"], - [capsuleBranch("v"), ret(fromDyn(ref("v")))], - ); - const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]); - const getter = getHelper( - "%dyn.class.property.0", - "x", - [capsuleBranch("p.0")], - call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))), - ); - const setter = setHelper("%dyn.class.property.1", "y"); - const write = setter.body[0] as Extract; - const keySet = write.expr as Extract; - keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0"))); - // A converted value that is not the receiver stays a helper call. - const foreign = getHelper( - "%dyn.class.property.2", - "x", - [], - toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)), - ); - const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign); - expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); - expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); - expect(sites.site("%dyn.class.property.2")).toBeNull(); -}); - -test("a bag helper may first rebind its parameter to a bag of it", () => { - // Programs with subclasses normalize the value before the class branches. - const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]); - const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc }); - const normalized = helper( - "%dyn.class.readPropertiesOrEmpty.normalized", - ["v"], - [rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))], - ); - const replaced = helper( - "%dyn.class.readPropertiesOrEmpty.replaced", - ["v"], - [rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))], - ); - const sites = sitesOf( - normalize, - normalized, - replaced, - getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))), - getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))), - // The property helper itself normalizes its receiver first, too. - getHelper("%dyn.class.property.2", "x", [ - { kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc }, - capsuleBranch("p.0"), - ]), - ); - expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); - expect(sites.site("%dyn.class.property.1")).toBeNull(); - expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" }); -}); - -test("keyed helpers may test capsules between the key's coercion and the read", () => { - const read = (object: IrExpr, key: string): IrExpr => ({ - kind: "libCall", - fn: "js.fromDyn", - args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }], - type: { kind: "jsval" }, - loc, - }); - const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({ - kind: "if", - cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc }, - then: [ret(read(object, "p.key"))], - else_: null, - loc, - }); - const keyed = (name: string, between: IrStmt[]): IrFunction => - helper( - name, - ["p.0", "p.key"], - [ - capsuleBranch("p.0"), - guard("symbol", "p.key", call(identityBag.name, ref("p.0"))), - guard("nullish", "p.0", ref("p.0")), - { - kind: "varDecl", - localId: "key.string", - init: { - kind: "libCall", - fn: "dyn.toStringCoerce", - args: [ref("p.key")], - type: STRING, - loc, - }, - loc, - }, - ...between, - ret(read(call(identityBag.name, ref("p.0")), "key.string")), - ], - ["key.string"], - ); - const errorBranch: IrStmt = { - kind: "if", - cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc }, - then: [ret(ref("p.0"))], - else_: null, - loc, - }; - const sites = sitesOf( - keyed("%dyn.class.computed.0", []), - keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]), - keyed("%dyn.class.computed.2", [errorBranch]), - ); - expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" }); - expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" }); - expect(sites.site("%dyn.class.computed.2")).toBeNull(); -}); - test("any other prefix or tail keeps the helper call", () => { const errorBranch: IrStmt = { kind: "if", @@ -371,57 +237,3 @@ test("bag helpers that exit early for values no class branch matches are still t expect(sites.site("%dyn.class.property.2")).toBeNull(); expect(sites.site("%dyn.class.property.3")).toBeNull(); }); - -test("helpers that answer plain values first with the same operation are cache sites", () => { - const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({ - kind: "dynTest", - test, - negated: true, - value: ref("p.0"), - type: BOOL, - loc, - }); - const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({ - kind: "if", - cond: test, - then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }], - else_: null, - loc, - }); - const store = (key: string): IrStmt[] => [ - { - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.keySet", - args: [ref("p.0"), str(key), ref("p.1")], - type: VOID, - loc, - }, - loc, - }, - ret(null), - ]; - const load = (key: string): IrStmt[] => [ - ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }), - ]; - const sites = sitesOf( - setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]), - getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]), - // An exit for another key, or one plain values do not take. - setHelper("%dyn.class.property.2", "x", [exit(store("y"))]), - setHelper("%dyn.class.property.3", "x", [ - exit(store("x"), { - kind: "dynTest", - test: "classCapsule", - value: ref("p.0"), - type: BOOL, - loc, - }), - ]), - ); - expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" }); - expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" }); - expect(sites.site("%dyn.class.property.2")).toBeNull(); - expect(sites.site("%dyn.class.property.3")).toBeNull(); -}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 48019636a..752316db3 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -61,68 +61,13 @@ export class JsIcSites { if (fn.params.some((p) => p.type.kind !== "dyn")) return null; const receiver = fn.params[0]?.localId; if (receiver === undefined) return null; - const second = fn.params[1]?.localId; - const exits: JsIcSite[] = []; let i = 0; - for (; i < fn.body.length; i++) { - const s = fn.body[i]!; - if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue; - const exit = this.plainExit(s, receiver, second); - if (exit === null) break; - exits.push(exit); - } + while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++; const tail = fn.body.slice(i); - let site: JsIcSite | null; - if (fn.params.length === 1) site = this.getTail(tail, receiver); - else if (fn.params.length !== 2 || second === undefined) return null; - else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); - if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null; - // An exit answers plain values with the operation the site caches. - return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null; - } - - /** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no - * else: an exit that runs the helper's generic operation directly on a - * value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R, - * K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a - * bag of it). */ - private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null { - const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean => - e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver); - if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null; - let body = s.then; - const inner = body[0]; - if ( - body.length === 1 && - inner?.kind === "if" && - inner.else_ === null && - negated(inner.cond, "funcKind") - ) - body = inner.then; - const [first, ret] = body; - if (body.length === 1 && first?.kind === "return") { - const read = unwrapFromDyn(first.value); - if ( - read?.kind !== "dynKeyGet" || - read.key.kind !== "strLit" || - !this.bagOf(read.value, receiver) - ) - return null; - return { kind: "get", key: read.key.value }; - } - if ( - body.length !== 2 || - first?.kind !== "exprStmt" || - ret?.kind !== "return" || - ret.value !== null - ) - return null; - const call = first.expr; - if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null; - const [bag, key, stored] = call.args; - if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null; - if (value === undefined || !isVar(stored, value)) return null; - return { kind: "set", key: key.value }; + if (fn.params.length === 1) return this.getTail(tail, receiver); + if (fn.params.length !== 2) return null; + const second = fn.params[1]!.localId; + return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); } /** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ? @@ -164,14 +109,10 @@ export class JsIcSites { /** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key); * if (receiver == null) return dynKeyGet(receiver, key); * const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read - * possibly under js.fromDyn. Capsule branches may precede the final read: - * a program that declares classes looks the string key up in each class's - * key table there. */ + * possibly under js.fromDyn. */ private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null { - const [symbol, nullish, decl] = tail; - const ret = tail[tail.length - 1]; - if (tail.length < 4) return null; - if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null; + const [symbol, nullish, decl, ret] = tail; + if (tail.length !== 4) return null; if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null; if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key)) return null; @@ -218,12 +159,9 @@ export class JsIcSites { } /** Whether `e` is the receiver itself for every ordinary object or array: - * the receiver, or a chain of property-bag helpers applied to it. A - * conversion between the dyn and jsval forms keeps the object it holds. */ + * the receiver, or a chain of property-bag helpers applied to it. */ private bagOf(e: IrExpr, receiver: string): boolean { if (isVar(e, receiver)) return true; - if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn")) - return e.args.length === 1 && this.bagOf(e.args[0]!, receiver); return ( e.kind === "call" && e.args.length === 1 && @@ -244,13 +182,6 @@ export class JsIcSites { return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); } - /** `param = `, such as the subclass normalization - * `value = %dyn.class.normalize(value)`, which leaves an ordinary object - * or array in `param`. */ - private rebinding(s: IrStmt, param: string): boolean { - return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param); - } - /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -273,10 +204,7 @@ export class JsIcSites { identity = fn.body .slice(0, -1) - .every( - (s) => - this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param), - ) && + .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 54ea13d26..47d641bff 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -64,50 +64,6 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); } }); -test("property sites keep their caches in programs that declare classes and subclasses", async () => { - const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-")); - try { - const entry = join(dir, "main.js"); - const outPath = join(dir, "main.ir.json"); - // Declared classes give every property helper class branches and - // property-bag lookups, and a subclass adds a normalization step; plain - // objects still read through the caches. - await writeFile( - entry, - ` -class Point { - constructor(x) { this.x = x; } -} -class Tagged extends Point { - constructor(x, tag) { super(x); this.tag = tag; } -} -function dyn(v) { return v; } -function read(o) { return o.x; } -function write(o, v) { o.x = v; } -function at(a, i) { return a[i]; } -var o = dyn({ x: 1 }); -var p = dyn(new Point(4)); -var t = dyn(new Tagged(6, "t")); -write(o, 3); -write(p, 5); -write(t, 7); -console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag")); -`, - ); - const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); - if (!result.ok) - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - const mod = deserializeModule(await readFile(outPath, "utf8")); - expect(validateModule(mod)).toEqual([]); - const llvm = emitLlvmModule(mod); - expect(body(llvm, "read")).toContain("@scr_js_load_probe("); - expect(body(llvm, "write")).toContain("@scr_js_store_probe("); - expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); - } finally { - await rm(dir, { recursive: true, force: true }); - } -}); - test("typed programs have no cache sites and keep the ordinary runtime", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-")); try { diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 1d874b31e..6fa20fdb8 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -195,13 +195,10 @@ bool scr_js_truthy(ScrJsVal value); * index and which keys an object lacks. * * Each property site of compiled dynamic-tier code owns one zeroed, - * thread-local ScrJsIc with static storage: the reset that frees a - * thread's shapes (at thread exit, and when a library session ends) clears - * every record that holds entries. The code at the site is fixed: it tests - * entry 0 inline and calls a probe for everything else. Probes never run - * user code: they answer from plain data properties or return a miss - * (NULL / false) with nothing done, and the site then runs its generic - * path. + * thread-local ScrJsIc. The code at the site is fixed: it tests entry 0 + * inline and calls a probe for everything else. Probes never run user + * code: they answer from plain data properties or return a miss (NULL / + * false) with nothing done, and the site then runs its generic path. * * load entry: shape = receiver shape; target = NULL for an own property * (slot indexes the receiver's entries), else the shape of diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 0e0ec0908..63929ab57 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -18,7 +18,6 @@ #include "scr_key.h" #include -#include /* The compiler's inline cache tests read these in place (64-bit targets; * backend/llvm/js-ic.ts). */ @@ -38,10 +37,10 @@ _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layo * take the generic path and its hash index. */ #define SCR_JS_SHAPE_MAX 64 -/* Shapes live until the thread exits or a library session ends, so a thread - * creates at most this many, holding at most this many key bytes. Once - * either budget is spent, objects whose key sequence has no shape yet stay - * unshaped; shapes that exist keep serving. */ +/* Shapes live until the thread exits, so a thread creates at most this + * many, holding at most this many key bytes. Once either budget is spent, + * objects whose key sequence has no shape yet stay unshaped; shapes that + * exist keep serving. */ #define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16) #define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20) @@ -64,25 +63,12 @@ static SCR_TL size_t scr_js_transition_count; static SCR_TL size_t scr_js_shape_key_bytes; static SCR_TL bool scr_js_shapes_registered; -/* The cache records that hold entries. Entries name shapes and prototype - * objects, so the reset that frees the shapes also clears these records: a - * library's next session runs the same sites with the same records. */ -static SCR_TL ScrJsIc **scr_js_filled_ics; -static SCR_TL size_t scr_js_filled_count; -static SCR_TL size_t scr_js_filled_capacity; - /* The shape word of an object whose key sequence has no shape within the * budgets. No cache entry names it, so its accesses take the probes, * which answer with a miss until the key set changes. */ static ScrJsShape scr_js_unshaped; static void scr_js_shapes_reset(void) { - for (size_t i = 0; i < scr_js_filled_count; i++) - memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]); - free(scr_js_filled_ics); - scr_js_filled_ics = NULL; - scr_js_filled_count = 0; - scr_js_filled_capacity = 0; while (scr_js_all_shapes) { ScrJsShape *shape = scr_js_all_shapes; scr_js_all_shapes = shape->all_next; @@ -206,16 +192,6 @@ static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t /* Fills a cache entry: `stale` (a matching entry whose hit failed) when * there is one, else a free or replaceable one. */ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { - if (ic->count == 0) { - if (scr_js_filled_count == scr_js_filled_capacity) { - size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64; - ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown); - if (!grown) scr_trap("scriptc: out of memory\n"); - scr_js_filled_ics = grown; - scr_js_filled_capacity = capacity; - } - scr_js_filled_ics[scr_js_filled_count++] = ic; - } uint32_t index; if (stale < ic->count) { index = stale; diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index 6b6934dea..a14aa9ff6 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -24,17 +24,11 @@ static ScrDyn *object_with(const ScrStr *key, double n) { return object; } -/* Cache records have static storage, as the compiled sites' do: the reset - * at exit clears every record that holds entries. Each check starts from - * an empty record. */ -static ScrJsIc load_ic; -static ScrJsIc store_ic; - /* Whether a load probe answers `object.key` and returns `n`. */ static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) { - ScrJsIc *ic = &load_ic; - memset(ic, 0, sizeof *ic); - ScrDyn *value = scr_js_load_probe(object, key, ic); + ScrJsIc ic; + memset(&ic, 0, sizeof ic); + ScrDyn *value = scr_js_load_probe(object, key, &ic); if (!value) return false; assert(value->kind == SCR_DYN_NUM && value->v.num == n); scr_dyn_release(value); @@ -72,13 +66,14 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { /* A store probe that would add a new shape leaves the add to the generic * store; one that replaces through an existing shape still runs. */ - memset(&store_ic, 0, sizeof store_ic); + ScrJsIc ic; + memset(&ic, 0, sizeof ic); ScrStr *added = scr_str_new("added", 5); ScrDyn *value = scr_dyn_new_num(8); - assert(!scr_js_store_probe(object, added, value, &store_ic)); + assert(!scr_js_store_probe(object, added, value, &ic)); assert(object->v.obj.len == 1); - memset(&store_ic, 0, sizeof store_ic); - assert(scr_js_store_probe(object, first, value, &store_ic)); + memset(&ic, 0, sizeof ic); + assert(scr_js_store_probe(object, first, value, &ic)); assert(probe_reads(object, first, 8)); scr_dyn_release(value); scr_str_release(added); @@ -95,9 +90,10 @@ static void immortal_objects_stay_unshaped(void) { object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); assert(object->v.obj.shape == NULL); - memset(&store_ic, 0, sizeof store_ic); + ScrJsIc ic; + memset(&ic, 0, sizeof ic); ScrDyn *value = scr_dyn_new_num(4); - assert(!scr_js_store_probe(object, key, value, &store_ic)); + assert(!scr_js_store_probe(object, key, value, &ic)); assert(object->v.obj.shape == NULL); scr_dyn_release(value); scr_str_release(key); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index f202c2ab7..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,8 +394,8 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. bench/dynamic-methods and - * bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/dynamic-methods is + * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -410,7 +410,6 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/thread-slots", "bench/channel-messages", "bench/dynamic-methods", - "bench/dynamic-properties", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index b60c3ac54..db4fabb3e 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -363,17 +363,6 @@ globals: undefined fenced expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); - test("K4: untyped property caches start empty in every session", async () => { - // The shapes a session's cache entries name are freed when it ends; - // the next session's sites must not find those entries. - const { archive, outDir } = await buildLibrary("js-ic-sessions", emission); - const probe = buildProbe("js-ic-sessions", archive, outDir); - const run = runProbe(probe); - expect(run.signal).toBeNull(); - expect(run.status).toBe(0); - expect(run.stdout).toBe("run: 20000\n".repeat(3)); - }); - /* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */ test("K5: a trap delivers to the sink exactly once, host survives", async () => { @@ -593,15 +582,6 @@ survived, sink_calls=1 expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); - platformTest("K10: property caches across sessions under ASan", async () => { - const { archive, outDir } = await buildLibrary("js-ic-sessions", emission, { sanitize: true }); - const probe = buildProbe("js-ic-sessions", archive, outDir, { sanitize: true }); - const run = runProbe(probe); - expect(run.signal).toBeNull(); - expect(run.status).toBe(0); - expect(run.stdout).toBe("run: 20000\n".repeat(3)); - }); - platformTest("K10: K5/K7 under ASan", async () => { const { archive, outDir } = await buildLibrary("traps", emission, { sanitize: true }); const probe = buildProbe("traps", archive, outDir, { sanitize: true }); diff --git a/tests/library-mode/js-ic-sessions/lib.ts b/tests/library-mode/js-ic-sessions/lib.ts deleted file mode 100644 index ebea59a54..000000000 --- a/tests/library-mode/js-ic-sessions/lib.ts +++ /dev/null @@ -1,6 +0,0 @@ -// The library's typed export over untyped code (work.mjs). -import { runAll } from "./work.mjs"; - -export function run(): number { - return runAll() as number; -} diff --git a/tests/library-mode/js-ic-sessions/probe.c b/tests/library-mode/js-ic-sessions/probe.c deleted file mode 100644 index 7dbcb56bf..000000000 --- a/tests/library-mode/js-ic-sessions/probe.c +++ /dev/null @@ -1,25 +0,0 @@ -/* Three sessions of a library whose untyped code reads and writes through - * property caches: each prints the same result, and under the sanitize - * flavor no session reads a cache entry the previous session freed. */ -#include -#include - -extern void kj_init(void); -extern void kj_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx); -extern void kj_collect(void); -extern double kj_run(void); - -static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { - (void)ctx; (void)addr; - printf("UNEXPECTED SINK: %.*s", (int)len, (const char *)msg); -} - -int main(void) { - kj_set_panic_sink(sink, NULL); - for (int session = 0; session < 3; session++) { - kj_init(); - printf("run: %.0f\n", kj_run()); - kj_collect(); - } - return 0; -} diff --git a/tests/library-mode/js-ic-sessions/profile.json b/tests/library-mode/js-ic-sessions/profile.json deleted file mode 100644 index fd802b42c..000000000 --- a/tests/library-mode/js-ic-sessions/profile.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "profile_format": 1, - "name": "conformance-js-ic-sessions", - "entry": "lib.ts", - "emission": "llvm", - "abi": { - "prefix": "kj_", - "init_symbol": "kj_init", - "sink_register_symbol": "kj_set_panic_sink", - "collect_symbol": "kj_collect", - "result_reset_symbol": null - }, - "exports": [{ "export": "run", "symbol": "kj_run", "params": [], "returns": "f64" }] -} diff --git a/tests/library-mode/js-ic-sessions/work.mjs b/tests/library-mode/js-ic-sessions/work.mjs deleted file mode 100644 index 5c1afcd18..000000000 --- a/tests/library-mode/js-ic-sessions/work.mjs +++ /dev/null @@ -1,26 +0,0 @@ -// Dynamic-tier property caches across library sessions: objects built empty -// and filled through untyped stores, read back through untyped loads, and a -// class instance at the same kind of sites. Each session must start with -// empty caches, since the shapes their entries named are freed when the -// previous session ends. -class Box { - constructor(v) { - this.v = v; - } -} -function fill(o, n) { - o.a = n; - o.b = n + 1; - return o; -} -function sum(o) { - return o.a + o.b; -} -function unbox(b) { - return b.v; -} -export function runAll() { - let total = 0; - for (let i = 0; i < 100; i++) total += sum(fill({}, i)); - return total + unbox(new Box(total)); -} From fc229822e7ac3ba33c0e31da0a044854cba5dbf7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:56:08 -0500 Subject: [PATCH 32/45] Support X25519 keys and crypto.diffieHellman X25519 KeyObjects come from generateKeyPair(Sync)("x25519"), which keeps the private key clamped as OpenSSL does, and from createPrivateKey and createPublicKey of PKCS#8, SubjectPublicKeyInfo, JWK (OKP, X25519) and raw keys. They export in the same forms. sign, verify, createSign and createVerify throw OpenSSL's "operation not supported for this keytype", with its library, reason and code properties. X25519 uses mbedTLS's Curve25519 group. crypto.diffieHellman({ privateKey, publicKey }[, callback]) computes the X25519 and ECDH secrets of two keys of one type, given as KeyObjects or in any form createPrivateKey and createPublicKey read. The checks run in Node's order: the options, the callback, both keys present, the key types when both are KeyObjects ("Incompatible key types for Diffie-Hellman"), then the public and private key inputs. Mismatched curves, a small-order X25519 point and key types other than EC and X25519 throw OpenSSL's errors. The callback form passes the secret, or the error without the OpenSSL properties, in a later macrotask, as Node's DHBitsJob does. createPublicKey also reads a private key's DER encodings (pkcs8 and sec1), which Node accepts there. --- .../limitations/builtin-apis/crypto.mdx | 12 +- .../src/generated/node-v24-compatibility.json | 6 +- .../generated/node-v24-internal.json | 15 +- .../ambient/scriptc-node-fallback.d.ts | 23 ++ .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../src/frontend/lowering/builtins/crypto.ts | 4 +- .../src/frontend/lowering/surfaces.ts | 4 +- packages/compiler/src/frontend/type-mapper.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/runtime-features.ts | 1 + packages/compiler/src/library/ir-policy.ts | 1 + packages/compiler/surface-manifest.json | 4 +- .../tests/corpus/7450-crypto-x25519.mjs.json | 6 + .../corpus/7451-crypto-diffie-hellman.ts.json | 6 + ...7452-crypto-diffie-hellman-errors.mjs.json | 6 + .../node-types/diffie-hellman.ts.json | 6 + packages/runtime/src/scr_crypto.h | 3 + packages/runtime/src/scr_pki.c | 339 ++++++++++++++++-- tests/corpus/7450-crypto-x25519.mjs | 90 +++++ tests/corpus/7451-crypto-diffie-hellman.ts | 55 +++ .../7452-crypto-diffie-hellman-errors.mjs | 61 ++++ tests/fixtures/node-types/diffie-hellman.ts | 19 + tests/fixtures/node-types/fenced.ts | 7 +- .../__snapshots__/node-types-fenced.txt | 35 +- .../corpus-costs/7450-crypto-x25519.mjs.json | 1 + .../7451-crypto-diffie-hellman.ts.json | 1 + ...7452-crypto-diffie-hellman-errors.mjs.json | 1 + tests/harness/project-config.test.ts | 18 + 29 files changed, 656 insertions(+), 73 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json create mode 100644 tests/corpus/7450-crypto-x25519.mjs create mode 100644 tests/corpus/7451-crypto-diffie-hellman.ts create mode 100644 tests/corpus/7452-crypto-diffie-hellman-errors.mjs create mode 100644 tests/fixtures/node-types/diffie-hellman.ts create mode 100644 tests/harness/corpus-costs/7450-crypto-x25519.mjs.json create mode 100644 tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json create mode 100644 tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 964ca4ec9..329aa6809 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -27,22 +27,22 @@ A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error. `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. - `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. - RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. - Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. - EC and Ed25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. - Raw X25519, X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. + Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. + Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. Sign and Verify values accumulate `update()` data and are not writable streams. - Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. + Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. `publicEncrypt`/`privateDecrypt` remain unsupported. The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index fc5826666..1039bc3d8 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11476,9 +11476,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 83c318a0a..b3d91e453 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -20345,8 +20345,19 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.diffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.diffieHellman", + "tests": [ + "tests/corpus/1358-crypto-random.ts", + "tests/corpus/1534-crypto-hash-chain.ts", + "tests/corpus/2556-crypto-introspection.ts", + "tests/corpus/2890-crypto-static-utility.ts", + "tests/harness/npm-static.test.ts", + "tests/corpus/node24-crypto-sha2.ts", + "tests/corpus/node24-crypto-kdf.ts", + "tests/corpus/node24-crypto-kdf-errors.ts", + "tests/corpus/node24-crypto-kdf-options.ts" + ] }, "anchorSource": "exact" }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 10ed35e68..cb08a4965 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -2992,6 +2992,24 @@ declare module "crypto" { options: ED25519KeyPairOptions<"der", "der">, ): KeyPairSyncResult; export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; + export interface X25519KeyPairOptions { + publicKeyEncoding: { type: "spki"; format: PubF }; + privateKeyEncoding: { type: "pkcs8"; format: PrivF }; + } + export function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync(type: "x25519", options?: {}): KeyPairKeyObjectResult; + export function generateKeyPair( + type: "x25519", + options: {} | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; export function generateKeyPair( type: "ed25519", options: {} | undefined, @@ -3020,6 +3038,11 @@ declare module "crypto" { export interface VerifyKeyObjectInput extends SigningOptions { key: KeyObject; } + export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): Buffer; + export function diffieHellman( + options: { privateKey: KeyObject; publicKey: KeyObject }, + callback: (err: Error | null, secret: Buffer) => void, + ): void; export function sign( algorithm: string | null | undefined, data: ArrayBufferView, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9cdc36cf5..ade8aea5e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1070,6 +1070,7 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync", "crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async", "crypto.signOneshot": "scr_crypto_sign_oneshot", + "crypto.diffieHellman": "scr_crypto_diffie_hellman", "crypto.verifyOneshot": "scr_crypto_verify_oneshot", "crypto.createSign": "scr_crypto_create_sign", "crypto.createVerify": "scr_crypto_create_verify", @@ -1654,6 +1655,7 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "crypto.generateKeyPairCb", + "crypto.diffieHellman", "crypto.checkPrimeCb", "crypto.generatePrimeCb", "process.emitWarning", diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 051ec18c4..15c057462 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1454,6 +1454,7 @@ function lowerPkiCall( ], sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"], verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"], + diffieHellman: ["crypto.diffieHellman", 1, 2, "diffieHellman(options[, callback])"], createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"], createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"], }; @@ -1474,6 +1475,7 @@ function lowerPkiCall( return { kind: "libCall", fn, args: values, type: VOID, loc }; const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (result === null || result.kind === "dyn") return call; + // diffieHellman's callback form answers undefined. + if (result === null || result.kind === "dyn" || result.kind === "void") return call; return lowerer.coerceInto(expr, call, result); } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44564f2f1..a54b41fd6 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1563,6 +1563,7 @@ export const BUILTIN_MODULE_FNS: Record< generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID }, sign: { fn: "crypto.signOneshot", params: [], result: DYN }, verify: { fn: "crypto.verifyOneshot", params: [], result: DYN }, + diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN }, pbkdf2Sync: { fn: "crypto.pbkdf2", params: [BYTES_U8, BYTES_U8, F64, F64, STRING], @@ -3101,9 +3102,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "createSign, createVerify, createECDH, and createDiffieHellman", ]), ), - diffieHellman: - "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + - "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 45b9572b6..781c032e7 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -183,6 +183,7 @@ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ "PrivateKeyInput", "PublicKeyInput", "RSAKeyPairKeyObjectOptions", + "X25519KeyPairOptions", "RSAKeyPairOptions", "SignKeyObjectInput", "SignPrivateKeyInput", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index b517d43a1..5dd437c7a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1031,6 +1031,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairCb", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.diffieHellman", "crypto.createSign", "crypto.createVerify", ]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e174238e2..2703f4014 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4874,6 +4874,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN }, "crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID }, "crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.diffieHellman": { argTypes: [DYN, DYN], result: DYN }, "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, "crypto.createSign": { argTypes: [DYN], result: DYN }, "crypto.createVerify": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index ca7fb7129..d5d2626c5 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -732,6 +732,7 @@ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairSync", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.diffieHellman", ]); /** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index e8fad6dd1..ac5d5e47b 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -63,6 +63,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.diffieHellman", "crypto.diffieHellman"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 3387217b5..75541b8e5 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -568,9 +568,7 @@ "id": "node-builtin.crypto.diffieHellman", "kind": "node-builtin", "name": "crypto.diffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets" + "status": "static" }, { "id": "node-builtin.crypto.generateKey", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json new file mode 100644 index 000000000..f156f7530 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7450-crypto-x25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json new file mode 100644 index 000000000..593054dd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7451-crypto-diffie-hellman.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json new file mode 100644 index 000000000..7cb78758b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7452-crypto-diffie-hellman-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json new file mode 100644 index 000000000..fb8a7a740 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/diffie-hellman.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index f61d27406..03a9dd109 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -298,6 +298,9 @@ ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options); void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback); ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key); ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature); +/* crypto.diffieHellman(options[, callback]): the shared secret, or undefined + * with the callback, which receives it in a later macrotask. */ +ScrDyn *scr_crypto_diffie_hellman(ScrDyn *options, ScrDyn *callback); ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm); ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm); bool scr_crypto_is_asymmetric_key(const ScrDyn *value); diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 94a0000e7..a1d5b98a6 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -1,7 +1,8 @@ /* node:crypto asymmetric keys and signatures over the vendored mbedTLS * public-key layer: createPrivateKey, createPublicKey, generateKeyPairSync - * and generateKeyPair for "rsa", "ec" and "ed25519" keys, the - * PrivateKeyObject and PublicKeyObject handles (type, asymmetricKeyType, + * and generateKeyPair for "rsa", "ec", "ed25519" and "x25519" keys, + * crypto.diffieHellman (ECDH and X25519), the PrivateKeyObject and + * PublicKeyObject handles (type, asymmetricKeyType, * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, * and the Sign and Verify objects of createSign and createVerify. Keys read * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, @@ -23,6 +24,7 @@ #include "mbedtls/asn1write.h" #include "mbedtls/base64.h" #include "mbedtls/bignum.h" +#include "mbedtls/ecdh.h" #include "mbedtls/ecdsa.h" #include "mbedtls/ecp.h" #include "mbedtls/md.h" @@ -40,6 +42,30 @@ static void sp_throw(int kind, const char *code, const char *message) { scr_throw_error_msg_code(kind, message, strlen(message), code); } +/* An OpenSSL error as Node's ThrowCryptoError reports it, + * "error::::", with own library, reason and code + * properties in Node's key order. A crypto job's callback gets the error + * without them (`props` false), as from Node's async jobs. */ +static void sp_throw_openssl(const char *packed, const char *library, const char *reason, const char *code, + bool props) { + char message[160]; + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + if (props) { + const char *keys[] = {"library", "reason", "code"}; + const char *values[] = {library, reason, code}; + for (size_t i = 0; i < 3; i++) { + if (i == 2) scr_error_set_code(e, code); + ScrStr *value = scr_str_new(values[i], strlen(values[i])); + scr_error_set_prop(e, keys[i], strlen(keys[i]), scr_dyn_new_str(value)); + scr_str_release(value); + } + } + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) { va_list ap, sized; va_start(ap, fmt); @@ -157,6 +183,8 @@ typedef struct SpKey { mbedtls_pk_context pk; /* RSA and EC keys */ bool private_key; bool ed25519; /* an Ed25519 key: ed_seed (private keys) and ed_pub */ + bool x25519; /* an X25519 key, in the same two fields: the private key's + * bytes as given, before clamping, and the public key */ uint8_t ed_seed[32]; uint8_t ed_pub[32]; } SpKey; @@ -215,6 +243,43 @@ static bool sp_is_ec(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBE static bool sp_is_ed(const SpKey *k) { return k->ed25519; } +static bool sp_is_x(const SpKey *k) { return k->x25519; } + +/* An Ed25519 or X25519 key: 32-byte private and public keys, no details. */ +static bool sp_is_okp(const SpKey *k) { return k->ed25519 || k->x25519; } + +/* X25519(scalar, point) per RFC 7748 over mbedTLS's Curve25519 group, or + * of the base point when point is NULL. False when the result is the + * all-zero value (a small-order point), which OpenSSL refuses. */ +static bool sp_x25519(const uint8_t scalar[32], const uint8_t *point, uint8_t out[32]) { + mbedtls_ecp_group grp; + mbedtls_ecp_point P, R; + mbedtls_mpi d; + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&P); + mbedtls_ecp_point_init(&R); + mbedtls_mpi_init(&d); + uint8_t k[32]; + memcpy(k, scalar, 32); + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; + size_t n = 0; + bool ok = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519) == 0 && + mbedtls_mpi_read_binary_le(&d, k, 32) == 0 && + (point ? mbedtls_ecp_point_read_binary(&grp, &P, point, 32) : mbedtls_ecp_copy(&P, &grp.G)) == 0 && + mbedtls_ecp_mul(&grp, &R, &d, &P, sp_random, NULL) == 0 && + mbedtls_ecp_point_write_binary(&grp, &R, MBEDTLS_ECP_PF_UNCOMPRESSED, &n, out, 32) == 0 && n == 32; + uint8_t any = 0; + for (size_t i = 0; ok && i < 32; i++) any |= out[i]; + mbedtls_platform_zeroize(k, sizeof k); + mbedtls_mpi_free(&d); + mbedtls_ecp_point_free(&R); + mbedtls_ecp_point_free(&P); + mbedtls_ecp_group_free(&grp); + return ok && any != 0; +} + typedef struct { const char *name; /* OpenSSL short name (namedCurve) */ const char *nist; /* EC_curve_nist2nid alias, or NULL */ @@ -241,7 +306,7 @@ static const SpCurve *sp_curve_of(const SpKey *k) { /* A parsed key is usable when it is RSA or EC on a curve above. */ static bool sp_key_supported(const SpKey *k) { - return sp_is_ed(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); + return sp_is_okp(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); } /* ── DER writing ──────────────────────────────────────────────────────── */ @@ -260,6 +325,11 @@ static const uint8_t sp_ed_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x3 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20}; static const uint8_t sp_ed_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00}; +/* X25519's, which differ in the OID's last byte (1.3.101.110). */ +static const uint8_t sp_x_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, + 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20}; +static const uint8_t sp_x_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, + 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00}; /* Writes prefix || 32 bytes at the end of buf, as the mbedtls writers do. */ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8_t key[32], @@ -272,7 +342,8 @@ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8 } static int sp_write_spki_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) return sp_write_ed_der(sp_ed_spki_prefix, sizeof sp_ed_spki_prefix, k->ed_pub, buf, size); + if (sp_is_okp(k)) + return sp_write_ed_der(sp_is_x(k) ? sp_x_spki_prefix : sp_ed_spki_prefix, 12, k->ed_pub, buf, size); return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); } @@ -287,8 +358,8 @@ static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size) * parameters, or id-ecPublicKey with the curve OID), and the inner key, * which for EC omits the parameters field. */ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) - return sp_write_ed_der(sp_ed_pkcs8_prefix, sizeof sp_ed_pkcs8_prefix, k->ed_seed, buf, size); + if (sp_is_okp(k)) + return sp_write_ed_der(sp_is_x(k) ? sp_x_pkcs8_prefix : sp_ed_pkcs8_prefix, 16, k->ed_seed, buf, size); unsigned char inner[SP_DER_MAX]; size_t inner_len; if (sp_is_rsa(k)) { @@ -473,9 +544,9 @@ static ScrDyn *sp_b64url(const uint8_t *raw, size_t len) { static ScrDyn *sp_export_jwk(SpKey *k) { ScrDyn *jwk = scr_dyn_new_obj(); - if (sp_is_ed(k)) { + if (sp_is_okp(k)) { sp_set(jwk, "kty", sp_dyn_str(scr_str_new("OKP", 3))); - sp_set(jwk, "crv", sp_dyn_str(scr_str_new("Ed25519", 7))); + sp_set(jwk, "crv", sp_dyn_str(sp_is_x(k) ? scr_str_new("X25519", 6) : scr_str_new("Ed25519", 7))); sp_set(jwk, "x", sp_b64url(k->ed_pub, 32)); if (k->private_key) sp_set(jwk, "d", sp_b64url(k->ed_seed, 32)); return jwk; @@ -587,7 +658,8 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { bool ok = false; if (sp_is_str(kty, "OKP")) { const ScrDyn *crv = sp_opt(jwk, "crv"); - if (!sp_is_str(crv, "Ed25519")) { + bool x = sp_is_str(crv, "X25519"); + if (!x && !sp_is_str(crv, "Ed25519")) { sp_key_release(k); if (crv->kind != SCR_DYN_STR) { scr_dyn_prop_type_fail("key.crv", "of type string", crv); @@ -596,10 +668,13 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { sp_not_supported("JWK keys of this crv"); return NULL; } - k->ed25519 = true; + k->ed25519 = !x; + k->x25519 = x; if (private_key) { ok = sp_jwk_bytes(jwk, "d", k->ed_seed); - if (ok) { + if (ok && x) { + ok = sp_x25519(k->ed_seed, NULL, k->ed_pub); + } else if (ok) { const ScrPkiEd25519 *ed = sp_ed(); if (!ed) { sp_key_release(k); @@ -678,9 +753,10 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { /* The private key of a public SpKey derived from `k`. */ static SpKey *sp_public_of(SpKey *k) { - if (sp_is_ed(k)) { + if (sp_is_okp(k)) { SpKey *pub = sp_key_alloc(false); - pub->ed25519 = true; + pub->ed25519 = k->ed25519; + pub->x25519 = k->x25519; memcpy(pub->ed_pub, k->ed_pub, 32); return pub; } @@ -770,6 +846,24 @@ static bool sp_cert_spki(const unsigned char *der, size_t len, const unsigned ch * as public answers its public key. */ static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { *failed = false; + /* X25519 keys, which need no other unit. */ + if (len == 48 && memcmp(der, sp_x_pkcs8_prefix, 16) == 0) { + SpKey *k = sp_key_alloc(private_key); + k->x25519 = true; + memcpy(k->ed_seed, der + 16, 32); + if (!sp_x25519(k->ed_seed, NULL, k->ed_pub)) { + sp_key_release(k); + return NULL; + } + if (!private_key) mbedtls_platform_zeroize(k->ed_seed, 32); + return k; + } + if (!private_key && len == 44 && memcmp(der, sp_x_spki_prefix, 12) == 0) { + SpKey *k = sp_key_alloc(false); + k->x25519 = true; + memcpy(k->ed_pub, der + 12, 32); + return k; + } if (len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) { const ScrPkiEd25519 *ed = sp_ed(); if (!ed) { @@ -945,9 +1039,9 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { return NULL; } if (sp_is_str(format, "der")) { - bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || - sp_is_str(type, "sec1") - : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + /* createPublicKey also reads a private key's encodings. */ + bool valid = sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") || + (!private_key && sp_is_str(type, "spki")); if (!valid) { sp_property_invalid("key.type", type); return NULL; @@ -997,15 +1091,14 @@ static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compre sp_incompatible_key_type(); return NULL; } - if (sp_is_ed(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); + if (sp_is_okp(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); const mbedtls_ecp_group *grp = &ec->MBEDTLS_PRIVATE(grp); unsigned char buf[1 + 2 * 66]; size_t n = (grp->nbits + 7) / 8; + int form = compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED; int rc = private_part ? mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), buf, n) - : mbedtls_ecp_point_write_binary( - grp, &ec->MBEDTLS_PRIVATE(Q), - compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED, &n, buf, sizeof buf); + : mbedtls_ecp_point_write_binary(grp, &ec->MBEDTLS_PRIVATE(Q), form, &n, buf, sizeof buf); ScrDyn *out = rc == 0 ? sp_dyn_buffer(buf, n) : NULL; mbedtls_platform_zeroize(buf, sizeof buf); if (!out) sp_unsupported_input(); @@ -1013,10 +1106,9 @@ static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compre } /* Node's named key types that this runtime does not generate or read: - * the other OKP curves and the post-quantum types. */ + * X448, Ed448 and the post-quantum types. */ static const char *const sp_other_named_types[] = { "ed448", - "x25519", "x448", "ml-dsa-44", "ml-dsa-65", @@ -1071,15 +1163,16 @@ static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const Scr sp_incompatible_key_type(); return NULL; } + bool x = sp_is_str(type, "x25519"); bool other = false; for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) other = other || sp_is_str(type, sp_other_named_types[i]); - if (!ec && !ed && !other) { + if (!ec && !ed && !x && !other) { ScrStr *s = type->v.str; scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid asymmetricKeyType: %.*s", (int)s->len, s->data); return NULL; } - bool ml = !ec && !ed && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; + bool ml = other && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; if (seed && !ml) { sp_incompatible_key_type(); return NULL; @@ -1099,7 +1192,16 @@ static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const Scr size_t len = (size_t)scr_bytes_byte_len(bytes); SpKey *k = sp_key_alloc(!raw_public); bool ok = false; - if (ed) { + if (x) { + k->x25519 = true; + if (len == 32 && raw_public) { + memcpy(k->ed_pub, p, 32); + ok = true; + } else if (len == 32) { + memcpy(k->ed_seed, p, 32); + ok = sp_x25519(k->ed_seed, NULL, k->ed_pub); + } + } else if (ed) { k->ed25519 = true; if (len == 32 && raw_public) { memcpy(k->ed_pub, p, 32); @@ -1254,7 +1356,7 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { static ScrDyn *sp_key_details(SpKey *k) { ScrDyn *details = scr_dyn_new_obj(); - if (sp_is_ed(k)) return details; + if (sp_is_okp(k)) return details; if (sp_is_rsa(k)) { mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); mbedtls_mpi e; @@ -1280,7 +1382,7 @@ static ScrDyn *sp_key_details(SpKey *k) { /* The DER that identifies a key for equals(): its private encoding for a * private key, its SubjectPublicKeyInfo otherwise. */ static int sp_identity_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); + if (sp_is_okp(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); return k->private_key ? sp_write_private_der(k, buf, size) : sp_write_spki_der(k, buf, size); } @@ -1326,6 +1428,7 @@ static ScrDyn *sp_key_get(void *h, const char *name, size_t len) { return k->private_key ? sp_dyn_str(scr_str_new("private", 7)) : sp_dyn_str(scr_str_new("public", 6)); if (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) return sp_is_ed(k) ? sp_dyn_str(scr_str_new("ed25519", 7)) + : sp_is_x(k) ? sp_dyn_str(scr_str_new("x25519", 6)) : sp_is_rsa(k) ? sp_dyn_str(scr_str_new("rsa", 3)) : sp_dyn_str(scr_str_new("ec", 2)); if (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) return sp_key_details(k); @@ -1543,7 +1646,7 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S /* The key generateKeyPair makes, as sp_keygen_check reads it. */ typedef struct { - enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519 } kind; + enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519, SP_GEN_X25519 } kind; double bits, exponent; /* RSA */ const SpCurve *curve; /* EC */ } SpKeyGen; @@ -1604,6 +1707,8 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen } } else if (sp_is_str(type, "ed25519")) { gen->kind = SP_GEN_ED25519; + } else if (sp_is_str(type, "x25519")) { + gen->kind = SP_GEN_X25519; } else { bool named = false; for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) @@ -1646,6 +1751,20 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { scr_pki_ed25519->public_key(k->ed_pub, k->ed_seed); return k; } + if (gen->kind == SP_GEN_X25519) { + /* OpenSSL stores a generated X25519 key clamped. */ + k->x25519 = true; + sp_random(NULL, k->ed_seed, 32); + k->ed_seed[0] &= 248; + k->ed_seed[31] &= 127; + k->ed_seed[31] |= 64; + if (!sp_x25519(k->ed_seed, NULL, k->ed_pub)) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + return k; + } if (gen->kind == SP_GEN_RSA) { const char *failure = sp_rsa_keygen_failure(gen->bits, gen->exponent); if (failure) { @@ -1977,6 +2096,16 @@ static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned c return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, sig, sig_len) == 0; } +/* X25519 keys neither sign nor verify: OpenSSL's error, after the key + * checks. Releases the key and answers true when it rejects. */ +static bool sp_reject_x25519(SpSigKey *k) { + if (!sp_is_x(k->key)) return false; + sp_key_release(k->key); + sp_throw_openssl("03000096", "digital envelope routines", "operation not supported for this keytype", + "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE", true); + return true; +} + static const char sp_data_types[] = "an instance of Buffer, TypedArray, or DataView"; /* Ed25519 signs the message itself: an algorithm other than null or @@ -1997,6 +2126,7 @@ ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key) { } SpSigKey k; if (!sp_sig_key(key, true, &k)) return NULL; + if (sp_reject_x25519(&k)) return NULL; ScrBytes *bytes = scr_bytes_buffer_source(data); if (sp_is_ed(k.key)) { ScrDyn *result = NULL; @@ -2034,6 +2164,7 @@ ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, scr_dyn_arg_type_fail("signature", sp_data_types, signature); return NULL; } + if (sp_reject_x25519(&k)) return NULL; ScrBytes *bytes = scr_bytes_buffer_source(data); ScrBytes *sig = scr_bytes_buffer_source(signature); if (sp_is_ed(k.key)) { @@ -2120,6 +2251,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy } SpSigKey k; if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), true, &k)) return NULL; + if (sp_reject_x25519(&k)) return NULL; if (sp_is_ed(k.key)) { sp_key_release(k.key); sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); @@ -2157,6 +2289,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy signature); return NULL; } + if (sp_reject_x25519(&k)) return NULL; const ScrStr *encoding = sp_encoding_arg(argc > 2 ? args[2] : NULL); ScrDyn *encoding_value = encoding ? scr_dyn_new_str((ScrStr *)encoding) : NULL; ScrBytes *sig = sp_source_bytes(signature, encoding_value); @@ -2323,6 +2456,156 @@ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *c scr_loop_defer(c); } +/* ── diffieHellman ────────────────────────────────────────────────────── */ + +/* The keys an options.privateKey or options.publicKey value can be. */ +static const char sp_dh_private_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, CryptoKey, or URL"; +static const char sp_dh_public_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"; + +/* Node's message names: a KeyObject's asymmetricKeyType. */ +static const char *sp_type_name(const SpKey *k) { + return sp_is_ed(k) ? "ed25519" : sp_is_x(k) ? "x25519" : sp_is_rsa(k) ? "rsa" : "ec"; +} + +/* A key input of diffieHellman's options: a primitive other than a string + * throws Node's ERR_INVALID_ARG_TYPE for `name`; other values read as + * sign and verify read their keys. */ +static bool sp_dh_key(const ScrDyn *value, const char *name, bool private_key, SpSigKey *out) { + out->key = NULL; + /* Node's key preparation destructures the value before it checks it. */ + if (value->kind == SCR_DYN_NULL) { + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot destructure property 'key' of 'key' as it is null."); + return false; + } + bool keyish = value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES || scr_buffer_storage_is(value) || + (value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_FUNC && scr_dyn_is_object(value)); + if (!keyish) { + scr_dyn_prop_type_fail(name, private_key ? sp_dh_private_types : sp_dh_public_types, value); + return false; + } + return sp_sig_key(value, private_key, out); +} + +/* The shared secret of two keys of one type: X25519, or ECDH's x + * coordinate at the field's size. NULL with OpenSSL's error pending. */ +static void sp_dh_failed(bool props) { + sp_throw_openssl("1C8000A4", "Provider routines", "failed during derivation", "ERR_OSSL_FAILED_DURING_DERIVATION", + props); +} + +static ScrDyn *sp_dh_compute(SpKey *priv, SpKey *pub, bool props) { + if (sp_is_x(priv) && sp_is_x(pub)) { + uint8_t out[32]; + if (!sp_x25519(priv->ed_seed, pub->ed_pub, out)) { + sp_dh_failed(props); + return NULL; + } + ScrDyn *secret = sp_dyn_buffer(out, 32); + mbedtls_platform_zeroize(out, sizeof out); + return secret; + } + if (!sp_is_ec(priv) || !sp_is_ec(pub)) { + sp_throw_openssl("03000096", "digital envelope routines", "operation not supported for this keytype", + "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE", props); + return NULL; + } + mbedtls_ecp_keypair *a = mbedtls_pk_ec(priv->pk); + const mbedtls_ecp_keypair *b = mbedtls_pk_ec(pub->pk); + if (a->MBEDTLS_PRIVATE(grp).id != b->MBEDTLS_PRIVATE(grp).id) { + sp_throw_openssl("1C8000CB", "Provider routines", "mismatching domain parameters", + "ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS", props); + return NULL; + } + mbedtls_mpi z; + mbedtls_mpi_init(&z); + unsigned char out[66]; + size_t n = (a->MBEDTLS_PRIVATE(grp).pbits + 7) / 8; + bool ok = mbedtls_ecdh_compute_shared(&a->MBEDTLS_PRIVATE(grp), &z, &b->MBEDTLS_PRIVATE(Q), + &a->MBEDTLS_PRIVATE(d), sp_random, NULL) == 0 && + mbedtls_mpi_write_binary(&z, out, n) == 0; + mbedtls_mpi_free(&z); + ScrDyn *secret = ok ? sp_dyn_buffer(out, n) : NULL; + mbedtls_platform_zeroize(out, sizeof out); + if (!secret) sp_dh_failed(props); + return secret; +} + +static void sp_dh_deliver(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *secret = sp_cap(self, 1); + ScrDyn *args[2] = {scr_dyn_new_null(), secret}; + ScrDyn *result = scr_dyn_call(callback, args, 2, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(args[0]); + scr_dyn_release(secret); + scr_dyn_release(callback); +} + +/* crypto.diffieHellman(options[, callback]) in Node's order: the options + * object, the callback, both keys present, the key types when both are + * KeyObjects, then the public and private key inputs. With a callback, + * the secret, or the error of computing it, arrives in a later macrotask. */ +ScrDyn *scr_crypto_diffie_hellman(ScrDyn *options, ScrDyn *callback) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + ScrDyn *result = NULL; + SpSigKey pub = {0}, priv = {0}; + if (!sp_check_object(plain, "options")) goto done; + bool async = !sp_absent(callback); + if (async && !scr_dyn_is_callable(callback)) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + goto done; + } + const ScrDyn *private_value = sp_opt(plain, "privateKey"); + const ScrDyn *public_value = sp_opt(plain, "publicKey"); + if (sp_absent(private_value)) { + sp_property_invalid("options.privateKey", private_value); + goto done; + } + if (sp_absent(public_value)) { + sp_property_invalid("options.publicKey", public_value); + goto done; + } + SpKey *a = sp_key_of(private_value), *b = sp_key_of(public_value); + if (a && b && a->private_key) { + const char *ta = sp_type_name(a), *tb = sp_type_name(b); + if (strcmp(ta, tb) != 0 || (!sp_is_ec(a) && !sp_is_x(a))) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY", + "Incompatible key types for Diffie-Hellman: %s and %s", ta, tb); + goto done; + } + } + if (!sp_dh_key(public_value, "options.publicKey", false, &pub) || + !sp_dh_key(private_value, "options.privateKey", true, &priv)) + goto done; + ScrDyn *secret = sp_dh_compute(priv.key, pub.key, !async); + if (!async) { + result = secret; + goto done; + } + void *deliver = (void *)sp_dh_deliver; + if (!secret) { + ScrCaught *caught = scr_exc_take(); + secret = scr_caught_to_dyn(caught); + scr_caught_release(caught); + deliver = (void *)sp_key_pair_fail; + } + ScrClosure *c = scr_closure_new(deliver, 2); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_dyn_retain(callback)); + c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[1], secret); + scr_loop_defer(c); + result = scr_dyn_retain(scr_dyn_undefined()); +done: + if (pub.key) sp_key_release(pub.key); + if (priv.key) sp_key_release(priv.key); + scr_dyn_release(plain); + return result; +} + /* ── the core for the WebCrypto layer (scr_pki.h) ─────────────────────── */ ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key) { return sp_key_retain(key); } diff --git a/tests/corpus/7450-crypto-x25519.mjs b/tests/corpus/7450-crypto-x25519.mjs new file mode 100644 index 000000000..180221390 --- /dev/null +++ b/tests/corpus/7450-crypto-x25519.mjs @@ -0,0 +1,90 @@ +// X25519 KeyObjects: generateKeyPair, PKCS#8, SubjectPublicKeyInfo, JWK +// and raw keys, RFC 7748's test vectors through crypto.diffieHellman, and +// Node's errors for signatures and incompatible encodings. +import * as crypto from "node:crypto"; + +function describe(value) { + if (Buffer.isBuffer(value)) return `Buffer(${value.length})`; + if (value instanceof crypto.KeyObject) return `${value.type} ${value.asymmetricKeyType} ${JSON.stringify(value.asymmetricKeyDetails)}`; + if (typeof value === "string") return value.split("\n")[0]; + if (value && typeof value === "object") return Object.entries(value).map(([key, field]) => `${key}=${describe(field)}`).join(" "); + return String(value); +} + +function attempt(label, run) { + try { + console.log(label, describe(run())); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +// RFC 7748, section 6.1 +const alicePrivate = Buffer.from("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", "hex"); +const alicePublic = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"; +const bobPrivate = Buffer.from("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", "hex"); +const bobPublic = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"; +const alice = crypto.createPrivateKey({ key: alicePrivate, format: "raw-private", asymmetricKeyType: "x25519" }); +const bob = crypto.createPrivateKey({ key: bobPrivate, format: "raw-private", asymmetricKeyType: "x25519" }); +console.log("alice public", Buffer.from(crypto.createPublicKey(alice).export({ format: "raw-public" })).toString("hex") === alicePublic); +console.log("bob public", Buffer.from(crypto.createPublicKey(bob).export({ format: "raw-public" })).toString("hex") === bobPublic); +const shared = crypto.diffieHellman({ privateKey: alice, publicKey: crypto.createPublicKey(bob) }); +console.log("shared", shared.toString("hex")); +console.log("both ways", crypto.diffieHellman({ privateKey: bob, publicKey: crypto.createPublicKey(alice) }).equals(shared)); +console.log("raw-private kept as given", Buffer.from(alice.export({ format: "raw-private" })).equals(alicePrivate)); + +// encodings +console.log("details", describe(alice), describe(crypto.createPublicKey(alice))); +const spki = alice.export({ format: "der", type: "pkcs8" }); +console.log("pkcs8", spki.length, spki.subarray(0, 16).toString("hex")); +const pub = crypto.createPublicKey(alice); +const spkiDer = pub.export({ format: "der", type: "spki" }); +console.log("spki", spkiDer.length, spkiDer.subarray(0, 12).toString("hex")); +console.log("pem", describe(alice.export({ format: "pem", type: "pkcs8" })), describe(pub.export({ format: "pem", type: "spki" }))); +const jwk = alice.export({ format: "jwk" }); +console.log("jwk", Object.keys(jwk).join(), jwk.kty, jwk.crv, jwk.x === Buffer.from(alicePublic, "hex").toString("base64url"), jwk.d === alicePrivate.toString("base64url")); +console.log("public jwk", JSON.stringify(pub.export({ format: "jwk" }))); +const fromPem = crypto.createPrivateKey(alice.export({ format: "pem", type: "pkcs8" })); +console.log("pem round trip", fromPem.equals(alice), crypto.createPublicKey(pub.export({ format: "pem", type: "spki" })).equals(pub)); +console.log("jwk round trip", crypto.createPrivateKey({ key: jwk, format: "jwk" }).equals(alice), crypto.createPublicKey({ key: pub.export({ format: "jwk" }), format: "jwk" }).equals(pub)); +console.log("public of pkcs8", crypto.createPublicKey({ key: spki, format: "der", type: "pkcs8" }).equals(pub)); +console.log("not ed25519", alice.equals(crypto.generateKeyPairSync("ed25519").privateKey)); + +// generateKeyPair +const pair = crypto.generateKeyPairSync("x25519"); +console.log("generated", describe(pair)); +const clamped = Buffer.from(pair.privateKey.export({ format: "raw-private" })); +console.log("generated clamped", (clamped[0] & 7) === 0, (clamped[31] & 0xc0) === 0x40); +attempt("pem pair", () => + crypto.generateKeyPairSync("x25519", { + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "der" }, + }), +); +attempt("raw pair", () => + crypto.generateKeyPairSync("x25519", { publicKeyEncoding: { format: "raw-public" }, privateKeyEncoding: { format: "raw-private" } }), +); +attempt("pkcs1", () => crypto.generateKeyPairSync("x25519", { publicKeyEncoding: { type: "pkcs1", format: "pem" } })); +attempt("sec1", () => crypto.generateKeyPairSync("x25519", { privateKeyEncoding: { type: "sec1", format: "pem" } })); +attempt("raw-seed", () => crypto.generateKeyPairSync("x25519", { privateKeyEncoding: { format: "raw-seed" } })); +attempt("X25519 spelling", () => crypto.generateKeyPairSync("X25519")); + +// what X25519 keys cannot do +const data = Buffer.from("x25519"); +attempt("sign", () => crypto.sign(null, data, alice)); +attempt("sign sha256", () => crypto.sign("sha256", data, alice)); +attempt("verify", () => crypto.verify(null, data, pub, Buffer.alloc(64))); +attempt("createSign", () => crypto.createSign("sha256").update(data).sign(alice)); +attempt("createVerify", () => crypto.createVerify("sha256").update(data).verify(pub, Buffer.alloc(64))); +attempt("hmac", () => crypto.createHmac("sha256", alice)); +attempt("export pkcs1", () => pub.export({ format: "pem", type: "pkcs1" })); +attempt("export raw-seed", () => alice.export({ format: "raw-seed" })); +attempt("short raw key", () => crypto.createPublicKey({ key: Buffer.alloc(31), format: "raw-public", asymmetricKeyType: "x25519" })); + +crypto.generateKeyPair("x25519", (err, publicKey, privateKey) => { + console.log("async", err, describe(publicKey), describe(privateKey)); + crypto.diffieHellman({ privateKey, publicKey: crypto.createPublicKey(bob) }, (err2, secret) => { + console.log("async secret", err2, describe(secret)); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7451-crypto-diffie-hellman.ts b/tests/corpus/7451-crypto-diffie-hellman.ts new file mode 100644 index 000000000..9648cd32b --- /dev/null +++ b/tests/corpus/7451-crypto-diffie-hellman.ts @@ -0,0 +1,55 @@ +// crypto.diffieHellman with typed KeyObjects: ECDH on every supported +// curve, checked against createECDH, X25519, the callback form, and +// Node's errors for incompatible key types, mismatched curves and a +// small-order X25519 public key. +import * as crypto from "node:crypto"; + +function attempt(label: string, run: () => Buffer): void { + try { + const secret = run(); + console.log(label, secret.length); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log(label, "throws", error.name, error.code, error.message); + } +} + +for (const namedCurve of ["prime256v1", "secp384r1", "secp521r1", "secp256k1"]) { + const a = crypto.generateKeyPairSync("ec", { namedCurve }); + const ecdh = crypto.createECDH(namedCurve); + const ecdhPublic = ecdh.generateKeys(); + const peer = crypto.createPublicKey({ key: ecdhPublic, format: "raw-public", asymmetricKeyType: "ec", namedCurve } as unknown as crypto.PublicKeyInput); + const secret = crypto.diffieHellman({ privateKey: a.privateKey, publicKey: peer }); + // The SubjectPublicKeyInfo ends with the uncompressed point. + const spki = a.publicKey.export({ format: "der", type: "spki" }); + const own = spki.subarray(spki.length - ecdhPublic.length); + console.log(namedCurve, secret.length, secret.equals(ecdh.computeSecret(own))); +} + +const x = crypto.generateKeyPairSync("x25519"); +const y = crypto.generateKeyPairSync("x25519"); +const xy = crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey }); +console.log("x25519", xy.length, xy.equals(crypto.diffieHellman({ privateKey: y.privateKey, publicKey: x.publicKey }))); +attempt("public key given as the private key", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.privateKey })); + +const p256 = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const p384 = crypto.generateKeyPairSync("ec", { namedCurve: "P-384" }); +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const ed = crypto.generateKeyPairSync("ed25519"); +attempt("x25519 and ec", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: p256.publicKey })); +attempt("ec and x25519", () => crypto.diffieHellman({ privateKey: p256.privateKey, publicKey: x.publicKey })); +attempt("rsa", () => crypto.diffieHellman({ privateKey: rsa.privateKey, publicKey: rsa.publicKey })); +attempt("ed25519", () => crypto.diffieHellman({ privateKey: ed.privateKey, publicKey: ed.publicKey })); +attempt("P-256 and P-384", () => crypto.diffieHellman({ privateKey: p256.privateKey, publicKey: p384.publicKey })); +attempt("public as private", () => crypto.diffieHellman({ privateKey: x.publicKey, publicKey: y.publicKey })); +const zero = crypto.createPublicKey({ key: Buffer.alloc(32), format: "raw-public", asymmetricKeyType: "x25519" } as unknown as crypto.PublicKeyInput); +attempt("small-order point", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: zero })); + +crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey }, (err, secret) => { + console.log("callback", err, secret.equals(xy)); + crypto.diffieHellman({ privateKey: x.privateKey, publicKey: zero }, (...args: unknown[]) => { + const error = args[0] as NodeJS.ErrnoException; + console.log("callback error", args.length, error.code, error.message); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7452-crypto-diffie-hellman-errors.mjs b/tests/corpus/7452-crypto-diffie-hellman-errors.mjs new file mode 100644 index 000000000..d8adc6442 --- /dev/null +++ b/tests/corpus/7452-crypto-diffie-hellman-errors.mjs @@ -0,0 +1,61 @@ +// crypto.diffieHellman's argument checks in Node's order, key inputs other +// than KeyObjects, and the own properties of the OpenSSL errors it and +// X25519 signatures throw. +import * as crypto from "node:crypto"; + +function attempt(label, run) { + try { + const result = run(); + console.log(label, Buffer.isBuffer(result) ? `Buffer(${result.length})` : String(result)); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +const x = crypto.generateKeyPairSync("x25519"); +const y = crypto.generateKeyPairSync("x25519"); +const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const other = crypto.generateKeyPairSync("ec", { namedCurve: "P-384" }); +const dh = (options, callback) => () => + callback === undefined ? crypto.diffieHellman(options) : crypto.diffieHellman(options, callback); + +attempt("no options", dh()); +attempt("number options", dh(5)); +attempt("array options", dh([])); +attempt("number callback", dh({ privateKey: x.privateKey, publicKey: y.publicKey }, 5)); +attempt("callback before keys", dh({}, 5)); +attempt("no private key", dh({ publicKey: y.publicKey })); +attempt("no public key", dh({ privateKey: x.privateKey })); +attempt("number public key", dh({ privateKey: x.privateKey, publicKey: 2 })); +attempt("number private key", dh({ privateKey: 1, publicKey: y.publicKey })); +attempt("null private key", dh({ privateKey: null, publicKey: y.publicKey })); +attempt("public key object as private", dh({ privateKey: x.publicKey, publicKey: y.publicKey })); +attempt("types checked first", dh({ privateKey: x.privateKey, publicKey: ec.publicKey })); + +// key inputs other than KeyObjects +const pkcs8 = x.privateKey.export({ format: "pem", type: "pkcs8" }); +const spki = y.publicKey.export({ format: "pem", type: "spki" }); +attempt("pem keys", dh({ privateKey: pkcs8, publicKey: spki })); +attempt("der key objects", dh({ + privateKey: { key: x.privateKey.export({ format: "der", type: "pkcs8" }), format: "der", type: "pkcs8" }, + publicKey: { key: y.publicKey.export({ format: "jwk" }), format: "jwk" }, +})); +attempt("public key from a private pem", dh({ privateKey: x.privateKey, publicKey: pkcs8 })); +attempt("same result", () => + crypto.diffieHellman({ privateKey: pkcs8, publicKey: spki }).equals(crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey })), +); +attempt("mixed types from pem", dh({ privateKey: x.privateKey, publicKey: ec.publicKey.export({ format: "pem", type: "spki" }) })); +attempt("mismatched curves", dh({ privateKey: ec.privateKey, publicKey: other.publicKey })); +const zero = crypto.createPublicKey({ key: Buffer.alloc(32), format: "raw-public", asymmetricKeyType: "x25519" }); +attempt("small-order point", dh({ privateKey: x.privateKey, publicKey: zero })); +attempt("sign", () => crypto.sign(null, Buffer.from("x"), x.privateKey)); + +// the callback form: argument errors throw, job errors arrive without +// the OpenSSL properties +attempt("callback returns", dh({ privateKey: x.privateKey, publicKey: y.publicKey }, (err, secret) => { + console.log("callback", err, secret.length); + crypto.diffieHellman({ privateKey: ec.privateKey, publicKey: other.publicKey }, (error) => { + console.log("callback mismatch", error.code, error.message, JSON.stringify(Object.keys(error))); + }); +})); +attempt("callback with a bad key throws", dh({ privateKey: 1, publicKey: y.publicKey }, () => console.log("never"))); diff --git a/tests/fixtures/node-types/diffie-hellman.ts b/tests/fixtures/node-types/diffie-hellman.ts new file mode 100644 index 000000000..d14fd5ffc --- /dev/null +++ b/tests/fixtures/node-types/diffie-hellman.ts @@ -0,0 +1,19 @@ +import { type KeyObject, createPublicKey, diffieHellman, generateKeyPair, generateKeyPairSync } from "node:crypto"; + +const alice = generateKeyPairSync("x25519"); +const bob = generateKeyPairSync("x25519"); +const secret: Buffer = diffieHellman({ privateKey: alice.privateKey, publicKey: bob.publicKey }); +console.log(alice.publicKey.asymmetricKeyType, secret.length, secret.equals(diffieHellman({ privateKey: bob.privateKey, publicKey: alice.publicKey }))); +const exported = alice.publicKey.export({ type: "spki", format: "pem" }); +const pem = typeof exported === "string" ? exported : exported.toString(); +const reread: KeyObject = createPublicKey(pem); +console.log(reread.equals(alice.publicKey), pem.split("\n")[0]); +const ec = generateKeyPairSync("ec", { namedCurve: "P-384" }); +const peer = generateKeyPairSync("ec", { namedCurve: "P-384" }); +console.log("ecdh", diffieHellman({ privateKey: ec.privateKey, publicKey: peer.publicKey }).length); +generateKeyPair("x25519", undefined, (err, publicKey, privateKey) => { + console.log("generated", err, publicKey.type, privateKey.asymmetricKeyType); + diffieHellman({ privateKey, publicKey: bob.publicKey }, (error, shared) => { + console.log("callback", error, shared.length); + }); +}); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 7c468de01..0ddf0ea3f 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -99,12 +99,9 @@ void callbackified; import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* DiffieHellman's Object.prototype members, and crypto.diffieHellman over - * asymmetric KeyObjects. */ -import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; +/* DiffieHellman's Object.prototype members. */ +import { createDiffieHellman } from "node:crypto"; createDiffieHellman(Buffer.from([23])).toString(); -declare const dhKey: KeyObject; -diffieHellman({ privateKey: dhKey, publicKey: dhKey }); /* WebCrypto beyond the secret-key slice: @types/node declares the whole * SubtleCrypto surface, and the methods outside the slice, such as * getPublicKey, fence by name. */ diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 562733f53..d0aa4a140 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -219,42 +219,33 @@ fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 100 | createECDH("sect163k1"); 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 102 | /* DiffieHellman's Object.prototype members, and crypto.diffieHellman over + 102 | /* DiffieHellman's Object.prototype members. */ hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:105:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet +fenced.ts:104:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet - 104 | import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; - 105 | createDiffieHellman(Buffer.from([23])).toString(); + 103 | import { createDiffieHellman } from "node:crypto"; + 104 | createDiffieHellman(Buffer.from([23])).toString(); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | declare const dhKey: KeyObject; + 105 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey(), setPrivateKey() and verifyError are supported -fenced.ts:107:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node but has no scriptc lowering yet +fenced.ts:109:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet - 106 | declare const dhKey: KeyObject; - 107 | diffieHellman({ privateKey: dhKey, publicKey: dhKey }); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 108 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole - - hint: crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets - -fenced.ts:112:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet - - 111 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 112 | await crypto.subtle.getPublicKey(key, ["verify"]); + 108 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 109 | await crypto.subtle.getPublicKey(key, ["verify"]); | ^~~~~~~~~~~~~~~~~~~~~~~~~~ - 113 | } + 110 | } hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -fenced.ts:117:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:114:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet - 116 | * compile; the other members fence by name. */ - 117 | const cores = navigator.hardwareConcurrency; + 113 | * compile; the other members fence by name. */ + 114 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 118 | void cores; + 115 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/corpus-costs/7450-crypto-x25519.mjs.json b/tests/harness/corpus-costs/7450-crypto-x25519.mjs.json new file mode 100644 index 000000000..727e2cdb6 --- /dev/null +++ b/tests/harness/corpus-costs/7450-crypto-x25519.mjs.json @@ -0,0 +1 @@ +{ "plain": 3330, "san": 4877 } diff --git a/tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json b/tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json new file mode 100644 index 000000000..a9347d13c --- /dev/null +++ b/tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json @@ -0,0 +1 @@ +{ "plain": 3265, "san": 6438 } diff --git a/tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json b/tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json new file mode 100644 index 000000000..72961a07f --- /dev/null +++ b/tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 2917, "san": 4460 } diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 4676499eb..fd2077a48 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1160,6 +1160,24 @@ test("node-types: asymmetric keys and signatures match Node under @types/node", expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: X25519 keys and diffieHellman match Node under @types/node", async () => { + const outDir = outDirFor("node-diffie-hellman"); + const entry = join(nodeTypesDir, "diffie-hellman.ts"); + const result = await compile(entry, { + outPath: join(outDir, "diffie-hellman"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: WebCrypto ECDSA, Ed25519 and RSA match Node under @types/node", async () => { const outDir = outDirFor("node-webcrypto-ecdsa"); const entry = join(nodeTypesDir, "webcrypto-ecdsa.ts"); From dddcbed335c5ff5cbead198cd2f8c5137cada11f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 14:09:39 -0700 Subject: [PATCH 33/45] Project zod 4 and @hono/zod-validator values at the declaration boundary The declaration-typed boundary already reaches zod 4 schemas and hono's c.req.valid(): zod 4's z.infer and hono's validator Input generics resolve to plain object types in the declaration world. Three gaps remained: - Boolean literal types printed as `false` (the TS7 facade leaves intrinsicName unset), so `{ ok: true } | { ok: false }` lost its true arm and `boolean | undefined` became `undefined | false`. The printer reads the literal's value. - `r.data` after `const r = S.safeParse(x); if (r.success)` never crossed: zod 4's result union is not data (its error arm is a class), and reads on projectable receivers were skipped on the assumption that something else types the receiver. Such reads now cross when the receiver is a package call whose type does not project, or a const bound to one. - Reads through bindings annotated with a projected type no longer get a redundant cast, and aliases are named only for casts that survive. New npm cases zod4-boundary and hono-zod-validator run real zod 4.6.7, hono 4.13.13 and @hono/zod-validator 0.9.1 (vendored file closures) under Node and natively; decl-boundary.test.ts checks that c.req.valid() is a checked cast with a path TypeError. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/decl-projection.ts | 111 +- .../npm/cases/hono-zod-validator/main.ts.json | 61 + .../npm/cases/zod4-boundary/main.ts.json | 6 + .../npm/cases/hono-zod-validator/lying.ts | 42 + .../npm/cases/hono-zod-validator/main.ts | 96 + .../npm/cases/hono-zod-validator/package.json | 1 + .../fixtures/npm/cases/zod4-boundary/main.ts | 146 + .../npm/cases/zod4-boundary/package.json | 1 + .../@hono/zod-validator/dist/index.d.mts | 43 + .../@hono/zod-validator/dist/index.mjs | 28 + .../@hono/zod-validator/package.json | 14 + .../npm/node_modules/hono/dist/compose.js | 50 + .../npm/node_modules/hono/dist/context.js | 381 +++ .../hono/dist/helper/cookie/index.js | 82 + .../npm/node_modules/hono/dist/hono-base.js | 350 +++ .../npm/node_modules/hono/dist/hono.js | 28 + .../node_modules/hono/dist/http-exception.js | 55 + .../npm/node_modules/hono/dist/index.js | 20 + .../npm/node_modules/hono/dist/request.js | 335 +++ .../hono/dist/request/constants.js | 4 + .../npm/node_modules/hono/dist/router.js | 35 + .../hono/dist/router/reg-exp-router/index.js | 3 + .../dist/router/reg-exp-router/matcher.js | 19 + .../hono/dist/router/reg-exp-router/node.js | 91 + .../router/reg-exp-router/prepared-router.js | 94 + .../hono/dist/router/reg-exp-router/router.js | 101 + .../hono/dist/router/reg-exp-router/trie.js | 73 + .../hono/dist/router/smart-router/router.js | 49 + .../hono/dist/router/trie-router/index.js | 2 + .../hono/dist/router/trie-router/node.js | 134 + .../hono/dist/router/trie-router/router.js | 15 + .../node_modules/hono/dist/router/utils.js | 4 + .../hono/dist/types/client/types.d.ts | 204 ++ .../node_modules/hono/dist/types/context.d.ts | 459 +++ .../hono/dist/types/hono-base.d.ts | 221 ++ .../node_modules/hono/dist/types/hono.d.ts | 20 + .../node_modules/hono/dist/types/index.d.ts | 6 + .../node_modules/hono/dist/types/request.d.ts | 326 ++ .../hono/dist/types/request/constants.d.ts | 3 + .../node_modules/hono/dist/types/router.d.ts | 98 + .../node_modules/hono/dist/types/types.d.ts | 214 ++ .../hono/dist/types/utils/body.d.ts | 75 + .../hono/dist/types/utils/headers.d.ts | 10 + .../hono/dist/types/utils/http-status.d.ts | 34 + .../hono/dist/types/utils/mime.d.ts | 73 + .../hono/dist/types/utils/types.d.ts | 62 + .../hono/dist/types/validator/index.d.ts | 3 + .../hono/dist/types/validator/utils.d.ts | 27 + .../hono/dist/types/validator/validator.d.ts | 18 + .../npm/node_modules/hono/dist/utils/body.js | 96 + .../node_modules/hono/dist/utils/buffer.js | 51 + .../node_modules/hono/dist/utils/constants.js | 7 + .../node_modules/hono/dist/utils/cookie.js | 125 + .../node_modules/hono/dist/utils/crypto.js | 34 + .../npm/node_modules/hono/dist/utils/html.js | 103 + .../npm/node_modules/hono/dist/utils/url.js | 201 ++ .../node_modules/hono/dist/validator/index.js | 2 + .../hono/dist/validator/validator.js | 64 + .../npm/node_modules/hono/package.json | 10 +- tests/fixtures/npm/node_modules/zod/LICENSE | 21 + .../fixtures/npm/node_modules/zod/index.d.cts | 3 + tests/fixtures/npm/node_modules/zod/index.js | 7 + .../npm/node_modules/zod/package.json | 23 + .../npm/node_modules/zod/v3/ZodError.d.cts | 164 + .../npm/node_modules/zod/v3/errors.d.cts | 5 + .../npm/node_modules/zod/v3/external.d.cts | 6 + .../zod/v3/helpers/enumUtil.d.cts | 8 + .../zod/v3/helpers/errorUtil.d.cts | 9 + .../zod/v3/helpers/parseUtil.d.cts | 78 + .../zod/v3/helpers/partialUtil.d.cts | 8 + .../zod/v3/helpers/typeAliases.d.cts | 2 + .../node_modules/zod/v3/helpers/util.d.cts | 85 + .../npm/node_modules/zod/v3/index.d.cts | 4 + .../npm/node_modules/zod/v3/locales/en.d.cts | 3 + .../node_modules/zod/v3/standard-schema.d.cts | 102 + .../npm/node_modules/zod/v3/types.d.cts | 1034 +++++++ .../node_modules/zod/v4/classic/checks.d.cts | 1 + .../npm/node_modules/zod/v4/classic/checks.js | 1 + .../node_modules/zod/v4/classic/coerce.d.cts | 17 + .../npm/node_modules/zod/v4/classic/coerce.js | 17 + .../node_modules/zod/v4/classic/compat.d.cts | 48 + .../npm/node_modules/zod/v4/classic/compat.js | 31 + .../zod/v4/classic/deep-partial.d.cts | 14 + .../zod/v4/classic/deep-partial.js | 13 + .../node_modules/zod/v4/classic/errors.d.cts | 29 + .../npm/node_modules/zod/v4/classic/errors.js | 57 + .../zod/v4/classic/external.d.cts | 19 + .../node_modules/zod/v4/classic/external.js | 17 + .../zod/v4/classic/from-json-schema.d.cts | 12 + .../zod/v4/classic/from-json-schema.js | 881 ++++++ .../node_modules/zod/v4/classic/in-out.d.cts | 8 + .../npm/node_modules/zod/v4/classic/in-out.js | 36 + .../node_modules/zod/v4/classic/index.d.cts | 3 + .../npm/node_modules/zod/v4/classic/iso.d.cts | 7 + .../npm/node_modules/zod/v4/classic/iso.js | 15 + .../node_modules/zod/v4/classic/parse.d.cts | 44 + .../npm/node_modules/zod/v4/classic/parse.js | 16 + .../node_modules/zod/v4/classic/schemas.d.cts | 816 +++++ .../node_modules/zod/v4/classic/schemas.js | 1535 ++++++++++ .../npm/node_modules/zod/v4/core/api.d.cts | 335 +++ .../npm/node_modules/zod/v4/core/api.js | 1107 +++++++ .../npm/node_modules/zod/v4/core/checks.d.cts | 294 ++ .../npm/node_modules/zod/v4/core/checks.js | 516 ++++ .../node_modules/zod/v4/core/compile.d.cts | 65 + .../npm/node_modules/zod/v4/core/compile.js | 1866 ++++++++++++ .../npm/node_modules/zod/v4/core/core.d.cts | 82 + .../npm/node_modules/zod/v4/core/core.js | 141 + .../npm/node_modules/zod/v4/core/doc.d.cts | 16 + .../npm/node_modules/zod/v4/core/doc.js | 38 + .../npm/node_modules/zod/v4/core/errors.d.cts | 225 ++ .../npm/node_modules/zod/v4/core/errors.js | 267 ++ .../npm/node_modules/zod/v4/core/index.d.cts | 19 + .../npm/node_modules/zod/v4/core/index.js | 19 + .../zod/v4/core/json-schema-generator.d.cts | 65 + .../zod/v4/core/json-schema-generator.js | 99 + .../zod/v4/core/json-schema-processors.d.cts | 65 + .../zod/v4/core/json-schema-processors.js | 843 ++++++ .../zod/v4/core/json-schema.d.cts | 89 + .../node_modules/zod/v4/core/json-schema.js | 1 + .../node_modules/zod/v4/core/memoizer.d.cts | 15 + .../npm/node_modules/zod/v4/core/memoizer.js | 311 ++ .../npm/node_modules/zod/v4/core/parse.d.cts | 52 + .../npm/node_modules/zod/v4/core/parse.js | 175 ++ .../node_modules/zod/v4/core/regexes.d.cts | 90 + .../npm/node_modules/zod/v4/core/regexes.js | 156 + .../node_modules/zod/v4/core/registries.d.cts | 35 + .../node_modules/zod/v4/core/registries.js | 51 + .../node_modules/zod/v4/core/schemas.d.cts | 1275 ++++++++ .../npm/node_modules/zod/v4/core/schemas.js | 2636 +++++++++++++++++ .../zod/v4/core/standard-schema.d.cts | 126 + .../zod/v4/core/to-json-schema.d.cts | 154 + .../zod/v4/core/to-json-schema.js | 651 ++++ .../npm/node_modules/zod/v4/core/util.d.cts | 280 ++ .../npm/node_modules/zod/v4/core/util.js | 873 ++++++ .../node_modules/zod/v4/core/versions.d.cts | 5 + .../npm/node_modules/zod/v4/core/versions.js | 5 + .../npm/node_modules/zod/v4/core/visit.js | 169 ++ .../npm/node_modules/zod/v4/index.d.cts | 3 + .../npm/node_modules/zod/v4/locales/ar.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ar.js | 111 + .../npm/node_modules/zod/v4/locales/az.d.cts | 5 + .../npm/node_modules/zod/v4/locales/az.js | 110 + .../npm/node_modules/zod/v4/locales/be.d.cts | 5 + .../npm/node_modules/zod/v4/locales/be.js | 168 ++ .../npm/node_modules/zod/v4/locales/bg.d.cts | 5 + .../npm/node_modules/zod/v4/locales/bg.js | 125 + .../npm/node_modules/zod/v4/locales/bn.d.cts | 5 + .../npm/node_modules/zod/v4/locales/bn.js | 113 + .../npm/node_modules/zod/v4/locales/ca.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ca.js | 112 + .../npm/node_modules/zod/v4/locales/ckb.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ckb.js | 133 + .../npm/node_modules/zod/v4/locales/cs.d.cts | 5 + .../npm/node_modules/zod/v4/locales/cs.js | 116 + .../npm/node_modules/zod/v4/locales/da.d.cts | 5 + .../npm/node_modules/zod/v4/locales/da.js | 120 + .../npm/node_modules/zod/v4/locales/de.d.cts | 5 + .../npm/node_modules/zod/v4/locales/de.js | 113 + .../npm/node_modules/zod/v4/locales/el.d.cts | 5 + .../npm/node_modules/zod/v4/locales/el.js | 112 + .../npm/node_modules/zod/v4/locales/en.d.cts | 5 + .../npm/node_modules/zod/v4/locales/en.js | 125 + .../npm/node_modules/zod/v4/locales/eo.d.cts | 5 + .../npm/node_modules/zod/v4/locales/eo.js | 114 + .../npm/node_modules/zod/v4/locales/es.d.cts | 5 + .../npm/node_modules/zod/v4/locales/es.js | 136 + .../npm/node_modules/zod/v4/locales/fa.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fa.js | 119 + .../npm/node_modules/zod/v4/locales/fi.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fi.js | 117 + .../node_modules/zod/v4/locales/fr-CA.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fr-CA.js | 112 + .../npm/node_modules/zod/v4/locales/fr.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fr.js | 129 + .../npm/node_modules/zod/v4/locales/gu.d.cts | 5 + .../npm/node_modules/zod/v4/locales/gu.js | 113 + .../npm/node_modules/zod/v4/locales/he.d.cts | 5 + .../npm/node_modules/zod/v4/locales/he.js | 221 ++ .../npm/node_modules/zod/v4/locales/hi.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hi.js | 111 + .../npm/node_modules/zod/v4/locales/hr.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hr.js | 126 + .../npm/node_modules/zod/v4/locales/hu.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hu.js | 113 + .../npm/node_modules/zod/v4/locales/hy.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hy.js | 158 + .../npm/node_modules/zod/v4/locales/id.d.cts | 5 + .../npm/node_modules/zod/v4/locales/id.js | 111 + .../node_modules/zod/v4/locales/index.d.cts | 63 + .../npm/node_modules/zod/v4/locales/index.js | 63 + .../npm/node_modules/zod/v4/locales/is.d.cts | 5 + .../npm/node_modules/zod/v4/locales/is.js | 114 + .../npm/node_modules/zod/v4/locales/it.d.cts | 5 + .../npm/node_modules/zod/v4/locales/it.js | 113 + .../npm/node_modules/zod/v4/locales/ja.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ja.js | 112 + .../npm/node_modules/zod/v4/locales/ka.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ka.js | 117 + .../npm/node_modules/zod/v4/locales/kh.d.cts | 5 + .../npm/node_modules/zod/v4/locales/kh.js | 5 + .../npm/node_modules/zod/v4/locales/km.d.cts | 5 + .../npm/node_modules/zod/v4/locales/km.js | 115 + .../npm/node_modules/zod/v4/locales/kn.d.cts | 5 + .../npm/node_modules/zod/v4/locales/kn.js | 116 + .../npm/node_modules/zod/v4/locales/ko.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ko.js | 116 + .../npm/node_modules/zod/v4/locales/lt.d.cts | 5 + .../npm/node_modules/zod/v4/locales/lt.js | 207 ++ .../npm/node_modules/zod/v4/locales/mk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/mk.js | 114 + .../npm/node_modules/zod/v4/locales/ms.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ms.js | 112 + .../npm/node_modules/zod/v4/locales/ne.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ne.js | 111 + .../npm/node_modules/zod/v4/locales/nl.d.cts | 5 + .../npm/node_modules/zod/v4/locales/nl.js | 115 + .../npm/node_modules/zod/v4/locales/nn.d.cts | 5 + .../npm/node_modules/zod/v4/locales/nn.js | 113 + .../npm/node_modules/zod/v4/locales/no.d.cts | 5 + .../npm/node_modules/zod/v4/locales/no.js | 113 + .../npm/node_modules/zod/v4/locales/ota.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ota.js | 114 + .../npm/node_modules/zod/v4/locales/pl.d.cts | 5 + .../npm/node_modules/zod/v4/locales/pl.js | 114 + .../npm/node_modules/zod/v4/locales/ps.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ps.js | 119 + .../node_modules/zod/v4/locales/pt-BR.d.cts | 5 + .../npm/node_modules/zod/v4/locales/pt-BR.js | 143 + .../npm/node_modules/zod/v4/locales/pt.d.cts | 5 + .../npm/node_modules/zod/v4/locales/pt.js | 142 + .../npm/node_modules/zod/v4/locales/ro.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ro.js | 122 + .../npm/node_modules/zod/v4/locales/ru.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ru.js | 168 ++ .../npm/node_modules/zod/v4/locales/sk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/sk.js | 116 + .../npm/node_modules/zod/v4/locales/sl.d.cts | 5 + .../npm/node_modules/zod/v4/locales/sl.js | 114 + .../npm/node_modules/zod/v4/locales/sv.d.cts | 5 + .../npm/node_modules/zod/v4/locales/sv.js | 115 + .../npm/node_modules/zod/v4/locales/ta.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ta.js | 115 + .../npm/node_modules/zod/v4/locales/tg.d.cts | 5 + .../npm/node_modules/zod/v4/locales/tg.js | 117 + .../npm/node_modules/zod/v4/locales/th.d.cts | 5 + .../npm/node_modules/zod/v4/locales/th.js | 115 + .../npm/node_modules/zod/v4/locales/tk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/tk.js | 107 + .../npm/node_modules/zod/v4/locales/tr.d.cts | 5 + .../npm/node_modules/zod/v4/locales/tr.js | 110 + .../npm/node_modules/zod/v4/locales/ua.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ua.js | 5 + .../npm/node_modules/zod/v4/locales/uk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/uk.js | 113 + .../npm/node_modules/zod/v4/locales/ur.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ur.js | 115 + .../npm/node_modules/zod/v4/locales/uz.d.cts | 5 + .../npm/node_modules/zod/v4/locales/uz.js | 113 + .../npm/node_modules/zod/v4/locales/vi.d.cts | 5 + .../npm/node_modules/zod/v4/locales/vi.js | 113 + .../npm/node_modules/zod/v4/locales/yo.d.cts | 5 + .../npm/node_modules/zod/v4/locales/yo.js | 112 + .../node_modules/zod/v4/locales/zh-CN.d.cts | 5 + .../npm/node_modules/zod/v4/locales/zh-CN.js | 114 + .../node_modules/zod/v4/locales/zh-TW.d.cts | 5 + .../npm/node_modules/zod/v4/locales/zh-TW.js | 112 + tests/harness/decl-boundary.test.ts | 28 +- tests/harness/npm.test.ts | 11 +- 268 files changed, 31356 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json create mode 100644 tests/fixtures/npm/cases/hono-zod-validator/lying.ts create mode 100644 tests/fixtures/npm/cases/hono-zod-validator/main.ts create mode 100644 tests/fixtures/npm/cases/hono-zod-validator/package.json create mode 100644 tests/fixtures/npm/cases/zod4-boundary/main.ts create mode 100644 tests/fixtures/npm/cases/zod4-boundary/package.json create mode 100644 tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/zod-validator/package.json create mode 100644 tests/fixtures/npm/node_modules/hono/dist/compose.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/context.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/hono-base.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/hono.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/http-exception.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/request.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/request/constants.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/utils.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/body.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/constants.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/html.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/url.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/validator/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/validator/validator.js create mode 100644 tests/fixtures/npm/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/node_modules/zod/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/index.js create mode 100644 tests/fixtures/npm/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/errors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/external.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/types.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/checks.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/compat.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/errors.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/external.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/iso.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/parse.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/schemas.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/api.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/api.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/checks.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/checks.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/compile.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/compile.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/core.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/core.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/doc.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/doc.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/errors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/errors.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/index.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-generator.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-generator.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-processors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-processors.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/memoizer.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/memoizer.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/parse.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/parse.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/regexes.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/regexes.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/registries.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/registries.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/schemas.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/schemas.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/standard-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/to-json-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/to-json-schema.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/util.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/util.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/versions.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/versions.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/visit.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ar.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ar.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/az.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/az.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/be.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/be.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bg.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bg.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bn.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bn.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ca.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ca.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ckb.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ckb.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/cs.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/cs.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/da.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/da.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/de.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/de.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/el.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/el.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/en.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/en.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/eo.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/eo.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/es.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/es.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fa.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fa.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fi.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fi.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr-CA.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr-CA.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/gu.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/gu.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/he.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/he.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hi.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hi.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hr.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hr.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hu.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hu.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hy.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hy.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/id.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/id.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/index.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/is.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/is.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/it.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/it.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ja.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ja.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ka.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ka.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kh.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kh.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/km.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/km.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kn.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kn.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ko.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ko.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/lt.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/lt.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/mk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/mk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ms.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ms.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ne.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ne.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nl.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nl.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nn.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nn.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/no.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/no.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ota.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ota.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pl.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pl.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ps.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ps.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt-BR.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt-BR.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ro.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ro.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ru.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ru.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sl.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sl.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sv.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sv.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ta.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ta.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tg.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tg.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/th.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/th.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tr.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tr.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ua.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ua.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ur.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ur.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uz.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uz.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/vi.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/vi.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/yo.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/yo.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-CN.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-CN.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-TW.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-TW.js diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index eaf7486df..02a300996 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -185,8 +185,24 @@ class TypePrinter { readonly declarations: string[] = []; private readonly inProgress = new Map(); + /** While set, printing registers no recursive alias (projectable()). */ + private dry = false; + constructor(private readonly checker: ts.TypeChecker) {} + /** Whether a type projects, without naming it in the generated file. */ + projectable(type: ts.Type): boolean { + if (type.flags & ANYISH) return false; + const was = this.dry; + this.dry = true; + try { + const text = this.print(type, 0); + return text !== null && text !== "unknown"; + } finally { + this.dry = was; + } + } + /** The alias naming a projectable type, or null. Top-level any/unknown * never projects. */ project(type: ts.Type): string | null { @@ -212,7 +228,12 @@ class TypePrinter { if (f & ts.TypeFlags.Null) return "null"; if (f & ts.TypeFlags.Undefined) return "undefined"; if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null; - if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false"; + // A boolean literal type carries its value (`intrinsicName` is not + // set for it by every checker facade). + if (f & ts.TypeFlags.BooleanLiteral) + return (type as ts.BooleanLiteralType).value === true || type.intrinsicName === "true" + ? "true" + : "false"; if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value)); if (f & ts.TypeFlags.NumberLiteral) { const v = Number(type.value); @@ -341,7 +362,7 @@ class TypePrinter { if (props.length > 0 && readonlyCount === props.length) return null; const text = `{ ${members.join("; ")} }`; if (frame.name !== null) { - this.declarations.push(`type ${frame.name} = ${text};`); + if (!this.dry) this.declarations.push(`type ${frame.name} = ${text};`); return frame.name; } return text; @@ -517,9 +538,17 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const declSf = input.declProgram.getSourceFile(file); if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue; const fileEdits: Edit[] = []; - /** Candidate expressions (by js node) with their alias. */ - const exprCandidates = new Map(); + /** Candidate expressions (by js node) with their declared type, named + * only for the casts that survive suppression. */ + const exprCandidates = new Map(); const exprOrder: ts.Node[] = []; + /** Reads whose projectable receiver the JavaScript world types `any`: + * kept only when nothing types the receiver. */ + const deferred = new Set(); + /** Package calls whose declared type does not project. */ + const unprojected = new Set(); + /** The declaration-world twin of every walked node. */ + const twins = new Map(); const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => { if ( @@ -603,8 +632,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { const recvDecl = typeAt(declChecker, d.expression); const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression); - const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null; + const recvProjectable = recvDecl !== null && printer.projectable(recvDecl); produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs)); + // A projectable receiver the JavaScript world types `any` is + // usually typed by another candidate. One that is a package value + // nothing projects stays `any`, such as a safeParse result narrowed + // to its success arm (the whole result is not data: its error arm + // is a class): the read crosses here instead, if the suppression + // below confirms the receiver stays untyped. + if (!produced) { + produced = true; + deferred.add(jsNode); + } } else if (ts.isAwaitExpression(d)) { const inner = unwrapTransparent(d.expression); produced = @@ -633,6 +672,12 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | declaredInPackage(resolveAlias(raw)); } if (!produced) return; + if (isCall && !deferred.has(jsNode)) { + // Recorded before the contextual-type test below: a package value + // whose type does not project stays `any` wherever it flows. + const t = typeAt(declChecker, d); + if (t === null || !printer.projectable(t)) unprojected.add(jsNode); + } // A consumer that takes anything needs no conversion. try { const ctx = declChecker.getContextualType(d); @@ -641,10 +686,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // no contextual type } const declType = typeAt(declChecker, d); - if (declType === null) return; - const alias = printer.project(declType); - if (alias === null) return; - exprCandidates.set(jsNode, alias); + if (declType === null || !printer.projectable(declType)) return; + exprCandidates.set(jsNode, declType); exprOrder.push(jsNode); }; @@ -703,6 +746,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const walk = (jsNode: ts.Node, declNode: ts.Node): void => { if (jsNode.kind !== declNode.kind) return; + twins.set(jsNode, declNode); if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) { if (considerType(jsNode, declNode)) return; } else { @@ -749,21 +793,67 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | result = willBeTyped(decl.initializer, depth + 1); else if (ts.isParameter(decl) && annotated.has(decl)) result = true; else if (ts.isBindingElement(decl)) result = false; + else if ( + (ts.isVariableDeclaration(decl) || ts.isParameter(decl)) && + decl.type !== undefined + ) + result = annotationWillBeTyped(decl.type); } symbolTyped.set(sym, result); return result; } return false; }; + /** An annotation in this file whose declaration-world type projects: + * after the rewrite it names the projected structure (a package type + * is replaced by its alias, a user alias of one has its right-hand + * side replaced). */ + const annotationWillBeTyped = (type: ts.TypeNode): boolean => { + const twin = twins.get(type); + if (twin === undefined) return false; + let declType: ts.Type | null; + try { + declType = declChecker.getTypeFromTypeNode(twin as ts.TypeNode); + } catch { + return false; + } + return declType !== null && printer.projectable(declType); + }; const receiverOf = (e: ts.Node): ts.Node | null => { if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression; if (ts.isIdentifier(e)) return null; return null; }; const kept: ts.Node[] = []; + /** A receiver that stays `any` after the rewrite: a package call whose + * type does not project, or a user const bound to one. */ + const staysUntyped = (e: ts.Node): boolean => { + const u = unwrapTransparent(e as ts.Expression); + if (unprojected.has(u)) return true; + if (!ts.isIdentifier(u)) return false; + let sym: ts.Symbol | undefined; + try { + sym = jsChecker.getSymbolAtLocation(u); + } catch { + return false; + } + const decls = sym?.declarations ?? []; + if (decls.length !== 1) return false; + const decl = decls[0]!; + return ( + ts.isVariableDeclaration(decl) && + decl.type === undefined && + decl.initializer !== undefined && + decl.parent !== undefined && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + unprojected.has(unwrapTransparent(decl.initializer)) + ); + }; for (const e of exprOrder) { const recv = receiverOf(e); if (recv !== null && willBeTyped(recv)) continue; + if (deferred.has(e) && (recv === null || !staysUntyped(recv))) continue; // An identifier candidate that is a user binding never gets here // (only package imports are candidates). kept.push(e); @@ -800,7 +890,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | ) continue; } - const alias = exprCandidates.get(e)!; + const alias = printer.project(exprCandidates.get(e)!); + if (alias === null) continue; const start = ts.tokenStartOfNode(e); const stmt = e.parent; let prefix = "("; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json new file mode 100644 index 000000000..72c2578e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json @@ -0,0 +1,61 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/hono-zod-validator/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1398, + "end": 1407 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1413, + "end": 1422 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 3716, + "end": 3725 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 4798, + "end": 4806 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 549, + "end": 557 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 5572, + "end": 5580 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json new file mode 100644 index 000000000..0d656e939 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod4-boundary/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/hono-zod-validator/lying.ts b/tests/fixtures/npm/cases/hono-zod-validator/lying.ts new file mode 100644 index 000000000..fa44c2e12 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/lying.ts @@ -0,0 +1,42 @@ +// Native-only (decl-boundary.test.ts): `c.req.valid()` is a checked +// boundary. z.custom checks nothing, so a body its declared type +// misdescribes fails the cast with a path TypeError, which hono's error +// handler reports; Node does not check. A union discriminated by boolean +// literals keeps both arms. +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +const Liar = z.custom<{ n: number }>(); +const Outcome = z.discriminatedUnion("ok", [ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), reason: z.string() }), +]); + +const app = new Hono(); +app.onError((err, c) => c.text(`${err instanceof TypeError} ${err.message}`, 500)); +app.post("/liar", zValidator("json", Liar), (c) => c.text(String(c.req.valid("json").n + 1))); +app.post("/outcome", zValidator("json", Outcome), (c) => { + const outcome = c.req.valid("json"); + return c.text(outcome.ok ? `value ${outcome.value + 1}` : `reason ${outcome.reason}`); +}); + +const requests: [string, unknown][] = [ + ["/liar", { n: 1 }], + ["/liar", { n: "x" }], + ["/outcome", { ok: true, value: 41 }], + ["/outcome", { ok: false, reason: "late" }], +]; + +async function main(): Promise { + for (const [path, body] of requests) { + const res = await app.request(path, { + method: "POST", + body: JSON.stringify(body), + headers: { "content-type": "application/json" }, + }); + console.log(path, res.status, await res.text()); + } +} + +void main(); diff --git a/tests/fixtures/npm/cases/hono-zod-validator/main.ts b/tests/fixtures/npm/cases/hono-zod-validator/main.ts new file mode 100644 index 000000000..9de69c482 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/main.ts @@ -0,0 +1,96 @@ +// A Hono app validated by @hono/zod-validator and zod 4.6, all compiled +// from their JavaScript: `c.req.valid(target)` hands the handler the +// parsed value, typed by the schema's z.infer through hono's declarations. +// Requests go through app.request, so no socket is involved. +import { zValidator } from "@hono/zod-validator"; +import { Hono, type Context } from "hono"; +import { z } from "zod"; + +const Book = z.strictObject({ + title: z.string().trim().min(1), + year: z.number().int().min(1450), + tags: z.array(z.string()).max(4).default([]), + rating: z.number().min(0).max(5).optional(), +}); +type Book = z.infer; +const ListQuery = z.object({ + minYear: z.coerce.number().int().optional(), + sort: z.enum(["title", "year"]).default("title"), + limit: z.coerce.number().int().min(1).max(10).default(3), +}); +const IdParam = z.object({ id: z.string().regex(/^b\d+$/) }); + +// Failed checks answer with the issues' paths and codes. +const reject = ( + result: { success: true } | { success: false; error: z.core.$ZodError }, + c: Context, +): Response | undefined => { + if (result.success) return undefined; + const paths = result.error.issues.map((i) => `${i.path.join(".")}:${i.code}`); + return c.json({ error: "invalid", paths }, 400); +}; + +const books = new Map(); +let nextId = 1; + +const app = new Hono(); +app.post( + "/books", + zValidator("json", Book, (result, c) => { + if (!result.success) { + const paths = result.error.issues.map((i) => `${i.path.join(".")}:${i.code}`); + return c.json({ error: "invalid", paths }, 400); + } + }), + (c) => { + const book = c.req.valid("json"); + book.tags.push("new"); + const id = `b${nextId++}`; + books.set(id, book); + const score = (book.rating ?? 0) * 2 + book.tags.length; + return c.json({ id, title: book.title.toUpperCase(), year: book.year, score }, 201); + }, +); +app.get("/books", zValidator("query", ListQuery, reject), (c) => { + const { minYear, sort, limit } = c.req.valid("query"); + const rows = [...books.values()] + .filter((b) => minYear === undefined || b.year >= minYear) + .sort((a, b) => (sort === "year" ? a.year - b.year : a.title < b.title ? -1 : 1)) + .slice(0, limit) + .map((b) => `${b.title}/${b.year}`); + return c.json({ sort, limit, rows }); +}); +app.get("/books/:id", zValidator("param", IdParam, reject), (c) => { + const id = c.req.valid("param").id; + const book = books.get(id); + if (book === undefined) return c.json({ error: "missing", id }, 404); + return c.json({ id, ...book }); +}); + +async function call(method: string, path: string, body?: unknown): Promise { + const init: RequestInit = { method }; + if (body !== undefined) { + init.body = JSON.stringify(body); + init.headers = { "content-type": "application/json" }; + } + const res = await app.request(path, init); + console.log(method, path, res.status, await res.text()); +} + +async function main(): Promise { + await call("POST", "/books", { title: " Dune ", year: 1965, tags: ["sf"], rating: 4.5 }); + await call("POST", "/books", { title: "Emma", year: 1815 }); + await call("POST", "/books", { title: "Ulysses", year: 1922, tags: ["modern"] }); + await call("POST", "/books", { title: "", year: 1200, extra: true }); + await call("POST", "/books", { title: "Bad", year: "x", tags: [1, 2, 3, 4, 5] }); + await call("GET", "/books"); + await call("GET", "/books?minYear=1900&sort=year&limit=5"); + await call("GET", "/books?limit=99"); + await call("GET", "/books/b2"); + await call("GET", "/books/b9"); + await call("GET", "/books/nope"); + const kept = books.get("b1")!; + console.log("stored", kept.tags.join(","), kept.title); +} + +void main(); diff --git a/tests/fixtures/npm/cases/hono-zod-validator/package.json b/tests/fixtures/npm/cases/hono-zod-validator/package.json new file mode 100644 index 000000000..27e5f1b4b --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/package.json @@ -0,0 +1 @@ +{ "name": "hono-zod-validator-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod4-boundary/main.ts b/tests/fixtures/npm/cases/zod4-boundary/main.ts new file mode 100644 index 000000000..b4079d1be --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-boundary/main.ts @@ -0,0 +1,146 @@ +// Values that cross from zod 4.6 (compiled from its JavaScript) into code +// typed by zod's declarations: z.infer, z.input and z.output, identity, +// aliasing and mutation after parse, values zod hands back unchanged, +// callbacks zod calls, typed array work, and caught failures. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Typed records: field reads, writes and array work on parsed data. +const Item = z.object({ + sku: z.string(), + price: z.number().nonnegative(), + qty: z.number().int(), + tags: z.array(z.string()).default([]), + note: z.string().optional(), + owner: z.string().nullable(), +}); +type Item = z.infer; +const items: Item[] = z.array(Item).parse([ + { sku: "a", price: 2.5, qty: 4, tags: ["x"], owner: null }, + { sku: "b", price: 10, qty: 1, owner: "ann", note: "fragile" }, + { sku: "c", price: 0.5, qty: 10, tags: ["x", "y"], owner: null }, +]); +const total = items.reduce((sum, it) => sum + it.price * it.qty, 0); +console.log("total", total); +const byTag = new Map(); +for (const it of items) for (const t of it.tags) byTag.set(t, (byTag.get(t) ?? 0) + it.qty); +console.log("byTag", [...byTag].map(([k, v]) => `${k}=${v}`).join(",")); +const sorted = [...items].sort((a, b) => b.price - a.price).map((it) => it.sku); +console.log("sorted", sorted.join(",")); +console.log("optional", items.map((it) => `${it.note ?? "-"}/${it.owner ?? "nobody"}`).join(",")); +console.log("keys", items.map((it) => Object.keys(it).join("+")).join(" ")); + +// Mutation after parse is visible through every reference. +const first = items[0]!; +first.qty += 1; +first.tags.push("z"); +console.log("mutated", items[0]!.qty, items[0]!.tags.join("|"), first === items[0]); +show("item", items[0]); + +// Strict objects reject unknown keys; the error is zod's, caught. +const Strict = z.strictObject({ name: z.string().trim().min(1), email: z.email() }); +type Strict = z.infer; +const person: Strict = Strict.parse({ name: " Ada ", email: "ada@example.com" }); +console.log("strict", person.name.length, person.email.toUpperCase()); +const rejected = Strict.safeParse({ name: "x", email: "x@example.com", extra: 1 }); +console.log("strict bad", rejected.success, rejected.error?.issues.map((i) => i.code).join(",")); + +// z.input and z.output differ across a transform and a default. +const Config = z.object({ + port: z.string().transform((s) => Number(s)), + host: z.string().default("localhost"), + debug: z.enum(["true", "false"]).transform((v) => v === "true"), +}); +type ConfigIn = z.input; +type ConfigOut = z.output; +const raw: ConfigIn = { port: "8080", debug: "true" }; +const config: ConfigOut = Config.parse(raw); +console.log("config", config.port + 1, config.host, config.debug, typeof config.port); + +// A value zod hands back unchanged keeps its identity and its mutations. +const Point = z.custom<{ x: number; y: number }>(); +const original = { x: 1, y: 2 }; +const same = Point.parse(original); +same.x = 10; +console.log("custom", original.x, same === original); +original.y = 20; +console.log("custom back", same.y); + +// A shared object returned by a transform: every parse sees the same one. +const shared = { hits: 0 }; +const Counter = z.string().transform(() => shared); +const c1 = Counter.parse("a"); +const c2 = Counter.parse("b"); +c1.hits += 1; +console.log("shared", c2.hits, shared.hits, c1 === c2); + +// A refine callback reads typed fields of the value zod checks. +const Range = z + .object({ lo: z.number(), hi: z.number() }) + .refine((r) => r.lo <= r.hi, { message: "lo must not exceed hi" }); +console.log("range ok", JSON.stringify(Range.parse({ lo: 1, hi: 3 }))); +const bad = Range.safeParse({ lo: 5, hi: 3 }); +console.log("range bad", bad.success ? "?" : bad.error.issues[0]?.message); + +// safeParse data, a discriminated union, and loose objects' extra keys. +const Event = z.discriminatedUnion("type", [ + z.object({ type: z.literal("click"), x: z.number(), y: z.number() }), + z.object({ type: z.literal("key"), key: z.string() }), +]); +type Event = z.infer; +const describe = (e: Event): string => (e.type === "click" ? `click@${e.x},${e.y}` : `key:${e.key}`); +const r = Event.safeParse({ type: "key", key: "Enter" }); +if (r.success) console.log("event", describe(r.data)); +const events = z.array(Event).parse([ + { type: "click", x: 1, y: 2 }, + { type: "key", key: "a" }, +]); +console.log("events", events.map(describe).join(" ")); +const Loose = z.looseObject({ id: z.number() }); +const loose = Loose.parse({ extra: true, id: 3, more: "m" }); +console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose)); + +// A union discriminated by boolean literals, read through a safeParse +// result narrowed to its success arm. +const Outcome = z.discriminatedUnion("ok", [ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), reason: z.string() }), +]); +type Outcome = z.infer; +const outcomes: Outcome[] = []; +for (const input of [{ ok: true, value: 41 }, { ok: false, reason: "late" }, { ok: "maybe" }]) { + const parsed = Outcome.safeParse(input); + if (parsed.success) outcomes.push(parsed.data); + else console.log("outcome bad", parsed.error.issues.length); +} +console.log("outcomes", outcomes.map((o) => (o.ok ? `value ${o.value + 1}` : `reason ${o.reason}`)).join(", ")); +const Flags = z.object({ beta: z.boolean().optional(), mode: z.enum(["a", "b"]) }); +const flagged = Flags.safeParse({ beta: true, mode: "b" }); +if (flagged.success) { + const flags = flagged.data; + console.log("flags", flags.beta === true ? "beta on" : "beta off", flags.mode); +} + +// Tuples, brands and coerced query-style input. +const Pair = z.tuple([z.string(), z.number()]); +const [label, count] = Pair.parse(["n", 4]); +console.log("tuple", label, count + 1); +const UserId = z.string().brand<"UserId">(); +const uid = UserId.parse("u_1"); +console.log("brand", uid.length, uid); +const Query = z.object({ page: z.coerce.number().int().min(1).default(1), q: z.string().optional() }); +const query = Query.parse({ page: "3" }); +console.log("query", query.page * 10, query.q === undefined, "q" in query); + +// async parse +async function main(): Promise { + const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2, owner: null }); + console.log("async", it.sku, it.price * it.qty, it.tags.length); + const failed = await Item.safeParseAsync({ sku: 1, price: -1, qty: 0.5, owner: null }); + console.log("async bad", failed.success, failed.error?.issues.map((i) => i.path.join(".")).join(",")); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod4-boundary/package.json b/tests/fixtures/npm/cases/zod4-boundary/package.json new file mode 100644 index 000000000..39bd81280 --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-boundary/package.json @@ -0,0 +1 @@ +{ "name": "zod4-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts new file mode 100644 index 000000000..6028477a3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts @@ -0,0 +1,43 @@ +import { InferInput } from "hono/validator"; +import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from "hono"; +import * as v3 from "zod/v3"; +import { ZodSafeParseResult } from "zod/v4"; +import * as v4 from "zod/v4/core"; +//#region src/index.d.ts +type ZodSchema = v3.ZodType | v4.$ZodType; +type ZodError = T extends v4.$ZodType ? v4.$ZodError> : v3.ZodError; +type ZodSafeParseResult$1 = T3 extends v4.$ZodType ? ZodSafeParseResult : v3.SafeParseReturnType; +type zInput = T extends v3.ZodType ? v3.input : T extends v4.$ZodType ? v4.input : never; +type zOutput = T extends v3.ZodType ? v3.output : T extends v4.$ZodType ? v4.output : never; +type zInfer = T extends v3.ZodType ? v3.infer : T extends v4.$ZodType ? v4.infer : never; +type ZodValidatorFailureBody = T extends v4.$ZodType ? Extract>, { + success: false; +}> : T extends v3.ZodType ? Extract, v3.output>, { + success: false; +}> : never; +type ZodValidatorFailureResponse = Response & TypedResponse, 400, "json">; +type Hook = (result: ({ + success: true; + data: T; +} | { + success: false; + error: ZodError; + data: T; +}) & { + target: Target; +}, c: Context) => Response | void | TypedResponse | Promise>; +type HasUndefined = undefined extends T ? true : false; +type ExtractValidationResponse = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends TypedResponse ? Response & TypedResponse : PR extends Response ? PR : PR extends undefined ? never : never : R extends TypedResponse ? Response & TypedResponse : R extends Response ? R : R extends undefined ? never : never : never; +type ValidationFunctionOption = { + validationFunction: (schema: T, value: ValidationTargets[Target]) => ZodSafeParseResult$1 | Promise>; +}; +type DefaultInput = { + in: HasUndefined extends true ? { [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput; } : { [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput; }; + out: { [K in Target]: Out; }; +}; +declare function zValidatorFunction, Out = zOutput, I extends Input = DefaultInput, V extends I = I>(target: Target, schema: T, hook?: undefined, options?: ValidationFunctionOption): MiddlewareHandler>; +declare function zValidatorFunction, In = zInput, Out = zOutput, I extends Input = DefaultInput, V extends I = I, InferredValue = zInfer>(target: Target, schema: T, hook?: HookFn, options?: ValidationFunctionOption): MiddlewareHandler>; +declare const zValidator: typeof zValidatorFunction; +//#endregion +export { Hook, zValidator }; +//# sourceMappingURL=index.d.mts.map \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs new file mode 100644 index 000000000..946d23d2a --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs @@ -0,0 +1,28 @@ +import { validator } from "hono/validator"; +//#region src/index.ts +function zValidatorFunction(target, schema, hook, options) { + const caseInsensitiveKeymap = target === "header" && ("_def" in schema || "_zod" in schema) ? Object.fromEntries(Object.keys("in" in schema ? schema.in.shape : schema.shape).map((key) => [key.toLowerCase(), key])) : void 0; + return validator(target, async (value, c) => { + let validatorValue = value; + if (caseInsensitiveKeymap) validatorValue = Object.fromEntries(Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])); + const result = options && options.validationFunction ? await options.validationFunction(schema, validatorValue) : await schema.safeParseAsync(validatorValue); + if (hook) { + const hookResult = await hook({ + data: validatorValue, + ...result, + target + }, c); + if (hookResult) { + if (hookResult instanceof Response) return hookResult; + if ("response" in hookResult) return hookResult.response; + } + } + if (!result.success) return c.json(result, 400); + return result.data; + }); +} +const zValidator = zValidatorFunction; +//#endregion +export { zValidator }; + +//# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json b/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json new file mode 100644 index 000000000..31355f555 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json @@ -0,0 +1,14 @@ +{ + "name": "@hono/zod-validator", + "version": "0.9.1", + "description": "Test fixture: the ESM build and its declarations.", + "type": "module", + "license": "MIT", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + } +} diff --git a/tests/fixtures/npm/node_modules/hono/dist/compose.js b/tests/fixtures/npm/node_modules/hono/dist/compose.js new file mode 100644 index 000000000..ec576df0f --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/compose.js @@ -0,0 +1,50 @@ +//#region src/compose.ts +/** +* Compose middleware functions into a single function based on `koa-compose` package. +* +* @template E - The environment type. +* +* @param {[[Function, unknown], unknown][] | [[Function]][]} middleware - An array of middleware functions and their corresponding parameters. +* @param {ErrorHandler} [onError] - An optional error handler function. +* @param {NotFoundHandler} [onNotFound] - An optional not-found handler function. +* +* @returns {(context: Context, next?: Next) => Promise} - A composed middleware function. +*/ +const compose = (middleware, onError, onNotFound) => { + return (context, next) => { + let index = -1; + return dispatch(0); + /** + * Dispatch the middleware functions. + * + * @param {number} i - The current index in the middleware array. + * + * @returns {Promise} - A promise that resolves to the context. + */ + async function dispatch(i) { + if (i <= index) throw new Error("next() called multiple times"); + index = i; + let res; + let isError = false; + let handler; + if (middleware[i]) { + handler = middleware[i][0][0]; + context.req.routeIndex = i; + } else handler = i === middleware.length && next || void 0; + if (handler) try { + res = await handler(context, () => dispatch(i + 1)); + } catch (err) { + if (err instanceof Error && onError) { + context.error = err; + res = await onError(err, context); + isError = true; + } else throw err; + } + else if (context.finalized === false && onNotFound) res = await onNotFound(context); + if (res && (context.finalized === false || isError)) context.res = res; + return context; + } + }; +}; +//#endregion +export { compose }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/context.js b/tests/fixtures/npm/node_modules/hono/dist/context.js new file mode 100644 index 000000000..7f8184af5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/context.js @@ -0,0 +1,381 @@ +import { HonoRequest } from "./request.js"; +import { HtmlEscapedCallbackPhase, resolveCallback } from "./utils/html.js"; +//#region src/context.ts +const TEXT_PLAIN = "text/plain; charset=UTF-8"; +const setDefaultContentType = (contentType, headers) => { + return { + "Content-Type": contentType, + ...headers + }; +}; +const createResponseInstance = (body, init) => new Response(body, init); +var Context = class { + #rawRequest; + #req; + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * + * @see {@link https://hono.dev/docs/api/context#env} + * + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + */ + env = {}; + #var; + finalized = false; + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * + * @see {@link https://hono.dev/docs/api/context#error} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + */ + error; + #status; + #executionCtx; + #res; + #layout; + #renderer; + #notFoundHandler; + #preparedHeaders; + #matchResult; + #path; + /** + * Creates an instance of the Context class. + * + * @param req - The Request object. + * @param options - Optional configuration options for the context. + */ + constructor(req, options) { + this.#rawRequest = req; + if (options) { + this.#executionCtx = options.executionCtx; + this.env = options.env; + this.#notFoundHandler = options.notFoundHandler; + this.#path = options.path; + this.#matchResult = options.matchResult; + } + } + /** + * `.req` is the instance of {@link HonoRequest}. + */ + get req() { + this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult); + return this.#req; + } + /** + * @see {@link https://hono.dev/docs/api/context#event} + * The FetchEvent associated with the current request. + * + * @throws Will throw an error if the context does not have a FetchEvent. + */ + get event() { + if (this.#executionCtx && "respondWith" in this.#executionCtx) return this.#executionCtx; + else throw Error("This context has no FetchEvent"); + } + /** + * @see {@link https://hono.dev/docs/api/context#executionctx} + * The ExecutionContext associated with the current request. + * + * @throws Will throw an error if the context does not have an ExecutionContext. + */ + get executionCtx() { + if (this.#executionCtx) return this.#executionCtx; + else throw Error("This context has no ExecutionContext"); + } + /** + * @see {@link https://hono.dev/docs/api/context#res} + * The Response object for the current request. + */ + get res() { + return this.#res ||= createResponseInstance(null, { headers: this.#preparedHeaders ??= new Headers() }); + } + /** + * Sets the Response object for the current request. + * + * @param _res - The Response object to set. + */ + set res(_res) { + if (this.#res && _res) { + _res = createResponseInstance(_res.body, _res); + for (const [k, v] of this.#res.headers.entries()) { + if (k === "content-type") continue; + if (k === "set-cookie") { + const cookies = this.#res.headers.getSetCookie(); + _res.headers.delete("set-cookie"); + for (const cookie of cookies) _res.headers.append("set-cookie", cookie); + } else _res.headers.set(k, v); + } + } + this.#res = _res; + this.finalized = true; + } + /** + * `.render()` can create a response within a layout. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + */ + render = (...args) => { + this.#renderer ??= (content) => this.html(content); + return this.#renderer(...args); + }; + /** + * Sets the layout for the response. + * + * @param layout - The layout to set. + * @returns The layout function. + */ + setLayout = (layout) => this.#layout = layout; + /** + * Gets the current layout for the response. + * + * @returns The current layout function. + */ + getLayout = () => this.#layout; + /** + * `.setRenderer()` can set the layout in the custom middleware. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + */ + setRenderer = (renderer) => { + this.#renderer = renderer; + }; + /** + * `.header()` can set headers. + * + * @see {@link https://hono.dev/docs/api/context#header} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * // Append multiple headers using the append option (e.g. Vary) + * c.header('Vary', 'Accept-Encoding', { append: true }) + * c.header('Vary', 'User-Agent', { append: true }) + * + * return c.body('Thank you for coming') + * }) + * ``` + */ + header = (name, value, options) => { + if (this.finalized) this.#res = createResponseInstance(this.#res.body, this.#res); + const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers(); + if (value === void 0) headers.delete(name); + else if (options?.append) headers.append(name, value); + else headers.set(name, value); + }; + status = (status) => { + this.#status = status; + }; + /** + * `.set()` can set the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is hot!!') + * await next() + * }) + * ``` + */ + set = (key, value) => { + this.#var ??= /* @__PURE__ */ new Map(); + this.#var.set(key, value); + }; + /** + * `.get()` can use the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + */ + get = (key) => { + return this.#var ? this.#var.get(key) : void 0; + }; + /** + * `.var` can access the value of a variable. + * + * @see {@link https://hono.dev/docs/api/context#var} + * + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + */ + get var() { + if (!this.#var) return {}; + return Object.fromEntries(this.#var); + } + #newResponse(data, arg, headers) { + let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders; + if (typeof arg === "object" && arg.headers) { + responseHeaders ??= new Headers(); + for (const [key, value] of new Headers(arg.headers)) if (key === "set-cookie") responseHeaders.append(key, value); + else responseHeaders.set(key, value); + } + if (headers) { + if (!responseHeaders) { + let count = 0; + for (const k in headers) if (++count > 1 || typeof headers[k] !== "string") { + responseHeaders = new Headers(); + break; + } + } + if (responseHeaders) for (const k in headers) { + const v = headers[k]; + if (typeof v === "string") responseHeaders.set(k, v); + else { + responseHeaders.delete(k); + for (const v2 of v) responseHeaders.append(k, v2); + } + } + } + const status = typeof arg === "number" ? arg : arg?.status ?? this.#status; + return createResponseInstance(data, { + status, + headers: responseHeaders ?? headers + }); + } + newResponse = (...args) => this.#newResponse(...args); + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * + * @see {@link https://hono.dev/docs/api/context#body} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + */ + body = (data, arg, headers) => this.#newResponse(data, arg, headers); + /** + * `.text()` can render text as `Content-Type:text/plain`. + * + * @see {@link https://hono.dev/docs/api/context#text} + * + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + */ + text = (text, arg, headers) => { + return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers)); + }; + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * + * @see {@link https://hono.dev/docs/api/context#json} + * + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + */ + json = (object, arg, headers) => { + return this.#newResponse(JSON.stringify(object), arg, setDefaultContentType("application/json", headers)); + }; + html = (html, arg, headers) => { + const res = (html) => this.#newResponse(html, arg, setDefaultContentType("text/html; charset=UTF-8", headers)); + return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html); + }; + /** + * `.redirect()` can Redirect, default status code is 302. + * + * @see {@link https://hono.dev/docs/api/context#redirect} + * + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + */ + redirect = (location, status) => { + const locationString = String(location); + this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)); + return this.newResponse(null, status ?? 302); + }; + /** + * `.notFound()` can return the Not Found Response. + * + * @see {@link https://hono.dev/docs/api/context#notfound} + * + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + */ + notFound = () => { + this.#notFoundHandler ??= () => createResponseInstance(); + return this.#notFoundHandler(this); + }; +}; +//#endregion +export { Context, TEXT_PLAIN }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js b/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js new file mode 100644 index 000000000..48f9cbfb7 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js @@ -0,0 +1,82 @@ +import { parse, parseSigned, serialize, serializeSigned } from "../../utils/cookie.js"; +//#region src/helper/cookie/index.ts +const getCookie = (c, key, prefix) => { + const cookie = c.req.raw.headers.get("Cookie"); + if (typeof key === "string") { + if (!cookie) return; + let finalKey = key; + if (prefix === "secure") finalKey = "__Secure-" + key; + else if (prefix === "host") finalKey = "__Host-" + key; + return parse(cookie, finalKey)[finalKey]; + } + if (!cookie) return {}; + return parse(cookie); +}; +const getSignedCookie = async (c, secret, key, prefix) => { + const cookie = c.req.raw.headers.get("Cookie"); + if (typeof key === "string") { + if (!cookie) return; + let finalKey = key; + if (prefix === "secure") finalKey = "__Secure-" + key; + else if (prefix === "host") finalKey = "__Host-" + key; + return (await parseSigned(cookie, secret, finalKey))[finalKey]; + } + if (!cookie) return {}; + return await parseSigned(cookie, secret); +}; +const generateCookie = (name, value, opt) => { + let cookie; + if (opt?.prefix === "secure") cookie = serialize("__Secure-" + name, value, { + path: "/", + ...opt, + secure: true + }); + else if (opt?.prefix === "host") cookie = serialize("__Host-" + name, value, { + ...opt, + path: "/", + secure: true, + domain: void 0 + }); + else cookie = serialize(name, value, { + path: "/", + ...opt + }); + return cookie; +}; +const setCookie = (c, name, value, opt) => { + const cookie = generateCookie(name, value, opt); + c.header("Set-Cookie", cookie, { append: true }); +}; +const generateSignedCookie = async (name, value, secret, opt) => { + let cookie; + if (opt?.prefix === "secure") cookie = await serializeSigned("__Secure-" + name, value, secret, { + path: "/", + ...opt, + secure: true + }); + else if (opt?.prefix === "host") cookie = await serializeSigned("__Host-" + name, value, secret, { + ...opt, + path: "/", + secure: true, + domain: void 0 + }); + else cookie = await serializeSigned(name, value, secret, { + path: "/", + ...opt + }); + return cookie; +}; +const setSignedCookie = async (c, name, value, secret, opt) => { + const cookie = await generateSignedCookie(name, value, secret, opt); + c.header("set-cookie", cookie, { append: true }); +}; +const deleteCookie = (c, name, opt) => { + const deletedCookie = getCookie(c, name, opt?.prefix); + setCookie(c, name, "", { + ...opt, + maxAge: 0 + }); + return deletedCookie; +}; +//#endregion +export { deleteCookie, generateCookie, generateSignedCookie, getCookie, getSignedCookie, setCookie, setSignedCookie }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/hono-base.js b/tests/fixtures/npm/node_modules/hono/dist/hono-base.js new file mode 100644 index 000000000..7bc399a89 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/hono-base.js @@ -0,0 +1,350 @@ +import { compose } from "./compose.js"; +import { getPath, getPathNoStrict, mergePath } from "./utils/url.js"; +import { Context } from "./context.js"; +import { METHODS } from "./router.js"; +import { COMPOSED_HANDLER } from "./utils/constants.js"; +//#region src/hono-base.ts +/** +* @module +* This module is the base module for the Hono object. +*/ +const notFoundHandler = (c) => { + return c.text("404 Not Found", 404); +}; +const errorHandler = (err, c) => { + if ("getResponse" in err) { + const res = err.getResponse(); + return c.newResponse(res.body, res); + } + console.error(err); + return c.text("Internal Server Error", 500); +}; +var Hono = class Hono { + get; + post; + put; + delete; + options; + patch; + query; + all; + on; + use; + router; + getPath; + _basePath = "/"; + #path = "/"; + routes = []; + constructor(options = {}) { + [...METHODS, "all"].forEach((method) => { + this[method] = (args1, ...args) => { + const methodName = method.toUpperCase(); + if (typeof args1 === "string") this.#path = args1; + else this.#addRoute(methodName, this.#path, args1); + args.forEach((handler) => { + this.#addRoute(methodName, this.#path, handler); + }); + return this; + }; + }); + this.on = (method, path, ...handlers) => { + for (const p of [path].flat()) { + this.#path = p; + for (const m of [method].flat()) { + const methodName = m.toUpperCase(); + for (const handler of handlers) this.#addRoute(methodName, this.#path, handler); + } + } + return this; + }; + this.use = (arg1, ...handlers) => { + if (typeof arg1 === "string") this.#path = arg1; + else { + this.#path = "*"; + handlers.unshift(arg1); + } + handlers.forEach((handler) => { + this.#addRoute("ALL", this.#path, handler); + }); + return this; + }; + const { strict, ...optionsWithoutStrict } = options; + Object.assign(this, optionsWithoutStrict); + this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict; + } + #clone() { + const clone = new Hono({ + router: this.router, + getPath: this.getPath + }); + clone.errorHandler = this.errorHandler; + clone.#notFoundHandler = this.#notFoundHandler; + clone.routes = this.routes; + return clone; + } + #notFoundHandler = notFoundHandler; + errorHandler = errorHandler; + /** + * `.route()` allows grouping other Hono instance in routes. + * + * @see {@link https://hono.dev/docs/api/routing#grouping} + * + * @param {string} path - base Path + * @param {Hono} app - other Hono instance + * @returns {Hono} routed Hono instance + * + * @example + * ```ts + * const app = new Hono() + * const app2 = new Hono() + * + * app2.get("/user", (c) => c.text("user")) + * app.route("/api", app2) // GET /api/user + * ``` + */ + route(path, app) { + const subApp = this.basePath(path); + app.routes.map((r) => { + let handler; + if (app.errorHandler === errorHandler) handler = r.handler; + else { + handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res; + handler[COMPOSED_HANDLER] = r.handler; + } + subApp.#addRoute(r.method, r.path, handler, r.basePath); + }); + return this; + } + /** + * `.basePath()` allows base paths to be specified. + * + * @see {@link https://hono.dev/docs/api/routing#base-path} + * + * @param {string} path - base Path + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + */ + basePath(path) { + const subApp = this.#clone(); + subApp._basePath = mergePath(this._basePath, path); + return subApp; + } + /** + * `.onError()` handles an error and returns a customized Response. + * + * @see {@link https://hono.dev/docs/api/hono#error-handling} + * + * @param {ErrorHandler} handler - request Handler for error + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ + onError = (handler) => { + this.errorHandler = handler; + return this; + }; + /** + * `.notFound()` allows you to customize a Not Found Response. + * + * @see {@link https://hono.dev/docs/api/hono#not-found} + * + * @param {NotFoundHandler} handler - request handler for not-found + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + */ + notFound = (handler) => { + this.#notFoundHandler = handler; + return this; + }; + /** + * `.mount()` allows you to mount applications built with other frameworks into your Hono application. + * + * @deprecated Use `mount()` from `hono/mount` instead. `.mount()` will be removed in v5. + * + * @see {@link https://hono.dev/docs/api/hono#mount} + * + * @param {string} path - base Path + * @param {Function} applicationHandler - other Request Handler + * @param {MountOptions} [options] - options of `.mount()` + * @returns {Hono} mounted Hono instance + * + * @example + * ```ts + * import { Router as IttyRouter } from 'itty-router' + * import { Hono } from 'hono' + * // Create itty-router application + * const ittyRouter = IttyRouter() + * // GET /itty-router/hello + * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) + * + * const app = new Hono() + * app.mount('/itty-router', ittyRouter.handle) + * ``` + * + * @example + * ```ts + * const app = new Hono() + * // Send the request to another application without modification. + * app.mount('/app', anotherApp, { + * replaceRequest: (req) => req, + * }) + * ``` + */ + mount(path, applicationHandler, options) { + let replaceRequest; + let optionHandler; + if (options) { + if (typeof options === "function") optionHandler = options; + else { + optionHandler = options.optionHandler; + if (options.replaceRequest === false) replaceRequest = (request) => request; + else replaceRequest = options.replaceRequest; + } + } + const getOptions = optionHandler ? (c) => { + const options = optionHandler(c); + return Array.isArray(options) ? options : [options]; + } : (c) => { + let executionContext = void 0; + try { + executionContext = c.executionCtx; + } catch {} + return [c.env, executionContext]; + }; + replaceRequest ||= (() => { + const mergedPath = mergePath(this._basePath, path); + const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length; + return (request) => { + const url = new URL(request.url); + url.pathname = this.getPath(request).slice(pathPrefixLength) || "/"; + return new Request(url, request); + }; + })(); + const handler = async (c, next) => { + const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c)); + if (res) return res; + await next(); + }; + this.#addRoute("ALL", mergePath(path, "*"), handler); + return this; + } + #addRoute(method, path, handler, baseRoutePath) { + path = mergePath(this._basePath, path); + const r = { + basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath, + path, + method, + handler + }; + this.router.add(method, path, [handler, r]); + this.routes.push(r); + } + #handleError(err, c) { + if (err instanceof Error) return this.errorHandler(err, c); + throw err; + } + #dispatch(request, executionCtx, env, method) { + if (method === "HEAD") return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))(); + const path = this.getPath(request, { env }); + const matchResult = this.router.match(method, path); + const c = new Context(request, { + path, + matchResult, + env, + executionCtx, + notFoundHandler: this.#notFoundHandler + }); + if (matchResult[0].length === 1) { + let res; + try { + res = matchResult[0][0][0][0](c, async () => { + c.res = await this.#notFoundHandler(c); + }); + } catch (err) { + return this.#handleError(err, c); + } + return res instanceof Promise ? res.then((resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c); + } + const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler); + return (async () => { + try { + const context = await composed(c); + if (!context.finalized) throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?"); + return context.res; + } catch (err) { + return this.#handleError(err, c); + } + })(); + } + /** + * `.fetch()` will be entry point of your app. + * + * @see {@link https://hono.dev/docs/api/hono#fetch} + * + * @param {Request} request - request Object of request + * @param {Env} env - env Object + * @param {ExecutionContext} executionCtx - context of execution + * @returns {Response | Promise} response of request + * + */ + fetch = (request, ...rest) => { + return this.#dispatch(request, rest[1], rest[0], request.method); + }; + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/docs/api/hono#request + */ + request = (input, requestInit, Env, executionCtx) => { + if (input instanceof Request) return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx); + input = input.toString(); + return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx); + }; + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @deprecated + * Use `fire` from `hono/service-worker` instead. + * ```ts + * import { Hono } from 'hono' + * import { fire } from 'hono/service-worker' + * + * const app = new Hono() + * // ... + * fire(app) + * ``` + * @see https://hono.dev/docs/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ + fire = () => { + addEventListener("fetch", (event) => { + event.respondWith(this.#dispatch(event.request, event, void 0, event.request.method)); + }); + }; +}; +//#endregion +export { Hono as HonoBase }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/hono.js b/tests/fixtures/npm/node_modules/hono/dist/hono.js new file mode 100644 index 000000000..9602aafc3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/hono.js @@ -0,0 +1,28 @@ +import { HonoBase as Hono$1 } from "./hono-base.js"; +import { RegExpRouter } from "./router/reg-exp-router/router.js"; +import "./router/reg-exp-router/index.js"; +import { SmartRouter } from "./router/smart-router/router.js"; +import { TrieRouter } from "./router/trie-router/router.js"; +import "./router/trie-router/index.js"; +//#region src/hono.ts +/** +* The Hono class extends the functionality of the HonoBase class. +* It sets up routing and allows for custom options to be passed. +* +* @template E - The environment type. +* @template S - The schema type. +* @template BasePath - The base path type. +*/ +var Hono = class extends Hono$1 { + /** + * Creates an instance of the Hono class. + * + * @param options - Optional configuration options for the Hono instance. + */ + constructor(options = {}) { + super(options); + this.router = options.router ?? new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()] }); + } +}; +//#endregion +export { Hono }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/http-exception.js b/tests/fixtures/npm/node_modules/hono/dist/http-exception.js new file mode 100644 index 000000000..800b530cd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/http-exception.js @@ -0,0 +1,55 @@ +//#region src/http-exception.ts +/** +* `HTTPException` must be used when a fatal error such as authentication failure occurs. +* +* @see {@link https://hono.dev/docs/api/exception} +* +* @param {StatusCode} status - status code of HTTPException +* @param {HTTPExceptionOptions} options - options of HTTPException +* @param {HTTPExceptionOptions["res"]} options.res - response of options of HTTPException +* @param {HTTPExceptionOptions["message"]} options.message - message of options of HTTPException +* @param {HTTPExceptionOptions["cause"]} options.cause - cause of options of HTTPException +* +* @example +* ```ts +* import { HTTPException } from 'hono/http-exception' +* +* // ... +* +* app.post('/auth', async (c, next) => { +* // authentication +* if (authorized === false) { +* throw new HTTPException(401, { message: 'Custom error message' }) +* } +* await next() +* }) +* ``` +*/ +var HTTPException = class extends Error { + res; + status; + /** + * Creates an instance of `HTTPException`. + * @param status - HTTP status code for the exception. Defaults to 500. + * @param options - Additional options for the exception. + */ + constructor(status = 500, options) { + super(options?.message, { cause: options?.cause }); + this.res = options?.res; + this.status = status; + } + /** + * Returns the response object associated with the exception. + * If a response object is not provided, a new response is created with the error message and status code. + * @returns The response object. + */ + getResponse() { + if (this.res) return new Response(this.res.body, { + status: this.status, + headers: this.res.headers + }); + return new Response(this.message, { status: this.status }); + } +}; +//#endregion +export { HTTPException }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/index.js b/tests/fixtures/npm/node_modules/hono/dist/index.js new file mode 100644 index 000000000..20c96572c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/index.js @@ -0,0 +1,20 @@ +import { Context } from "./context.js"; +import { Hono } from "./hono.js"; +//#region src/index.ts +/** +* @module +* +* Hono - Web Framework built on Web Standards +* +* @example +* ```ts +* import { Hono } from 'hono' +* const app = new Hono() +* +* app.get('/', (c) => c.text('Hono!')) +* +* export default app +* ``` +*/ +//#endregion +export { Context, Hono }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/request.js b/tests/fixtures/npm/node_modules/hono/dist/request.js new file mode 100644 index 000000000..bf130a518 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/request.js @@ -0,0 +1,335 @@ +import { HTTPException } from "./http-exception.js"; +import { GET_MATCH_RESULT } from "./request/constants.js"; +import { parseBody } from "./utils/body.js"; +import { getQueryParam, getQueryParams, tryDecodeURIComponent } from "./utils/url.js"; +//#region src/request.ts +var HonoRequest = class { + /** + * `.raw` can get the raw Request object. + * + * @see {@link https://hono.dev/docs/api/request#raw} + * + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + */ + raw; + #validatedData; + #matchResult; + routeIndex = 0; + /** + * `.path` can get the pathname of the request. + * + * @see {@link https://hono.dev/docs/api/request#path} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + */ + path; + bodyCache = {}; + constructor(request, path = "/", matchResult = [[]]) { + this.raw = request; + this.path = path; + this.#matchResult = matchResult; + } + param(key) { + return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams(); + } + #getDecodedParam(key) { + const paramKey = this.#matchResult[0][this.routeIndex]?.[1][key]; + const param = this.#getParamValue(paramKey); + return param && tryDecodeURIComponent(param); + } + #getAllDecodedParams() { + const decoded = {}; + const keys = Object.keys(this.#matchResult[0][this.routeIndex]?.[1] ?? {}); + for (const key of keys) { + const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]); + if (value !== void 0) decoded[key] = tryDecodeURIComponent(value); + } + return decoded; + } + #getParamValue(paramKey) { + return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey; + } + query(key) { + return getQueryParam(this.url, key); + } + queries(key) { + return getQueryParams(this.url, key); + } + header(name) { + if (name) return this.raw.headers.get(name) ?? void 0; + const headerData = Object.create(null); + this.raw.headers.forEach((value, key) => { + headerData[key] = value; + }); + return headerData; + } + async parseBody(options) { + return parseBody(this, options); + } + #cachedBody = (key) => { + const { bodyCache, raw } = this; + const cachedBody = bodyCache[key]; + if (cachedBody) return cachedBody; + for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((body) => { + if (anyCachedKey === "json") body = JSON.stringify(body); + const contentType = anyCachedKey === "formData" ? void 0 : raw.headers.get("content-type"); + return new Response(body, { headers: contentType ? { "Content-Type": contentType } : void 0 })[key](); + }); + return bodyCache[key] = raw[key](); + }; + /** + * `.json()` can parse Request body of type `application/json` + * + * @see {@link https://hono.dev/docs/api/request#json} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + */ + json() { + return this.#cachedBody("text").then((text) => JSON.parse(text)); + } + /** + * `.text()` can parse Request body of type `text/plain` + * + * @see {@link https://hono.dev/docs/api/request#text} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + */ + text() { + return this.#cachedBody("text"); + } + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * + * @see {@link https://hono.dev/docs/api/request#arraybuffer} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + */ + arrayBuffer() { + return this.#cachedBody("arrayBuffer"); + } + /** + * `.bytes()` parses the request body as a `Uint8Array`. + * + * @see {@link https://hono.dev/docs/api/request#bytes} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.bytes() + * }) + * ``` + */ + bytes() { + return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer)); + } + /** + * Parses the request body as a `Blob`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.blob(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#blob + */ + blob() { + return this.#cachedBody("blob"); + } + /** + * Parses the request body as `FormData`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.formData(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#formdata + */ + formData() { + return this.#cachedBody("formData"); + } + /** + * Adds validated data to the request. + * + * @param target - The target of the validation. + * @param data - The validated data to add. + */ + addValidatedData(target, data) { + (this.#validatedData ??= {})[target] = data; + } + valid(target) { + return this.#validatedData?.[target]; + } + /** + * `.url` can get the request url strings. + * + * @see {@link https://hono.dev/docs/api/request#url} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + */ + get url() { + return this.raw.url; + } + /** + * `.method` can get the method name of the request. + * + * @see {@link https://hono.dev/docs/api/request#method} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + */ + get method() { + return this.raw.method; + } + get [GET_MATCH_RESULT]() { + return this.#matchResult; + } + /** + * `.matchedRoutes` can return a matched route in the handler + * + * @deprecated + * + * Use matchedRoutes helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#matchedroutes} + * + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + */ + get matchedRoutes() { + return this.#matchResult[0].map(([[, route]]) => route); + } + /** + * `.routePath` can retrieve the path registered within the handler + * + * @deprecated + * + * Use routePath helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#routepath} + * + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + */ + get routePath() { + return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path; + } +}; +/** +* Clones a HonoRequest's underlying raw Request object. +* +* This utility handles both consumed and unconsumed request bodies: +* - If the request body hasn't been consumed, it uses the native `clone()` method +* - If the request body has been consumed, it reconstructs a new Request using cached body data +* +* This is particularly useful when you need to: +* - Process the same request body multiple times +* - Pass requests to external services after validation +* +* @param req - The HonoRequest object to clone +* @returns A Promise that resolves to a new Request object with the same properties +* @throws {HTTPException} If the request body was consumed directly via `req.raw` +* without using HonoRequest methods (e.g., `req.json()`, `req.text()`), making it +* impossible to reconstruct the body from cache +* +* @example +* ```ts +* // Clone after consuming the body (e.g., after validation) +* app.post('/forward', +* validator('json', (data) => data), +* async (c) => { +* const validated = c.req.valid('json') +* // Body has been consumed, but cloneRawRequest still works +* const clonedReq = await cloneRawRequest(c.req) +* return fetch('http://backend-service.com', clonedReq) +* } +* ) +* ``` +*/ +const cloneRawRequest = async (req) => { + if (!req.raw.bodyUsed) return req.raw.clone(); + const cacheKey = Object.keys(req.bodyCache)[0]; + if (!cacheKey) throw new HTTPException(500, { message: "Cannot clone request: body was already consumed and not cached. Please use HonoRequest methods (e.g., req.json(), req.text()) instead of consuming req.raw directly." }); + let body = await req[cacheKey](); + const headers = req.header(); + if (cacheKey === "json") { + body = JSON.stringify(body); + delete headers["content-length"]; + } else if (body instanceof FormData) { + delete headers["content-type"]; + delete headers["content-length"]; + } + const requestInit = { + body, + cache: req.raw.cache, + credentials: req.raw.credentials, + headers, + integrity: req.raw.integrity, + keepalive: req.raw.keepalive, + method: req.method, + mode: req.raw.mode, + redirect: req.raw.redirect, + referrer: req.raw.referrer, + referrerPolicy: req.raw.referrerPolicy, + signal: req.raw.signal + }; + return new Request(req.url, requestInit); +}; +//#endregion +export { HonoRequest, cloneRawRequest }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/request/constants.js b/tests/fixtures/npm/node_modules/hono/dist/request/constants.js new file mode 100644 index 000000000..0f2734437 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/request/constants.js @@ -0,0 +1,4 @@ +//#region src/request/constants.ts +const GET_MATCH_RESULT = Symbol(); +//#endregion +export { GET_MATCH_RESULT }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router.js b/tests/fixtures/npm/node_modules/hono/dist/router.js new file mode 100644 index 000000000..52ea7f5c2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router.js @@ -0,0 +1,35 @@ +//#region src/router.ts +/** +* @module +* This module provides types definitions and variables for the routers. +*/ +/** +* Constant representing all HTTP methods in uppercase. +*/ +const METHOD_NAME_ALL = "ALL"; +/** +* Constant representing all HTTP methods in lowercase. +*/ +const METHOD_NAME_ALL_LOWERCASE = "all"; +/** +* Array of supported HTTP methods. +*/ +const METHODS = [ + "get", + "post", + "put", + "delete", + "options", + "patch", + "query" +]; +/** +* Error message indicating that a route cannot be added because the matcher is already built. +*/ +const MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built."; +/** +* Error class representing an unsupported path error. +*/ +var UnsupportedPathError = class extends Error {}; +//#endregion +export { MESSAGE_MATCHER_IS_ALREADY_BUILT, METHODS, METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE, UnsupportedPathError }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js new file mode 100644 index 000000000..38dfdd48a --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js @@ -0,0 +1,3 @@ +import { RegExpRouter } from "./router.js"; +import { PreparedRegExpRouter, buildInitParams, serializeInitParams } from "./prepared-router.js"; +export { PreparedRegExpRouter, RegExpRouter, buildInitParams, serializeInitParams }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js new file mode 100644 index 000000000..97ac8435c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js @@ -0,0 +1,19 @@ +import "../../router.js"; +//#region src/router/reg-exp-router/matcher.ts +const emptyParam = []; +function match(method, path) { + const matchers = this.buildAllMatchers(); + const match = ((method, path) => { + const matcher = matchers[method] || matchers["ALL"]; + const staticMatch = matcher[2][path]; + if (staticMatch) return staticMatch; + const match = path.match(matcher[0]); + if (!match) return [[], emptyParam]; + const index = match.indexOf("", 1); + return [matcher[1][index], match]; + }); + this.match = match; + return match(method, path); +} +//#endregion +export { emptyParam, match }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js new file mode 100644 index 000000000..6738b9e98 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js @@ -0,0 +1,91 @@ +import { createNullObject } from "../utils.js"; +//#region src/router/reg-exp-router/node.ts +const LABEL_REG_EXP_STR = "[^/]+"; +const ONLY_WILDCARD_REG_EXP_STR = ".*"; +const TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)"; +const PATH_ERROR = Symbol(); +const regExpMetaChars = /* @__PURE__ */ new Set(".\\+*[^]$()"); +/** +* Sort order: +* 1. literal +* 2. special pattern (e.g. :label{[0-9]+}) +* 3. common label pattern (e.g. :label) +* 4. wildcard +*/ +function compareKey(a, b) { + if (a.length === 1) return b.length === 1 ? a < b ? -1 : 1 : -1; + if (b.length === 1) return 1; + if (a === ".*" || a === "(?:|/.*)") return b === "(?:|/.*)" ? -1 : 1; + else if (b === ".*" || b === "(?:|/.*)") return -1; + if (a === "[^/]+") return 1; + else if (b === "[^/]+") return -1; + return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length; +} +var Node = class Node { + #index; + #varIndex; + #children = createNullObject(); + insert(tokens, index, paramMap, context, isStatic) { + let node = this; + for (let i = 0, len = tokens.length; i < len; i++) { + const token = tokens[i]; + const pattern = token.length === 1 ? token === "*" ? i === len - 1 ? [ + "", + "", + ".*" + ] : [ + "", + "", + LABEL_REG_EXP_STR + ] : null : token === "/*" ? [ + "", + "", + TAIL_WILDCARD_REG_EXP_STR + ] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/); + let nextNode; + if (pattern) { + const name = pattern[1]; + let regexpStr = pattern[2] || "[^/]+"; + if (name && pattern[2]) { + if (regexpStr === ".*") throw PATH_ERROR; + regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:"); + if (/\((?!\?:)/.test(regexpStr)) throw PATH_ERROR; + if (regexpStr.length === 1 && regExpMetaChars.has(regexpStr)) throw PATH_ERROR; + } + nextNode = node.#children[regexpStr]; + if (!nextNode) { + if (regexpStr !== ".*" && regexpStr !== "(?:|/.*)") { + for (const k in node.#children) if ((regexpStr.length > 1 || k.length > 1) && k !== ".*" && k !== "(?:|/.*)") throw PATH_ERROR; + } + nextNode = node.#children[regexpStr] = new Node(); + } + if (name !== "") { + nextNode.#varIndex ??= context.varIndex++; + paramMap.push([name, nextNode.#varIndex]); + } + } else { + nextNode = node.#children[token]; + if (!nextNode) { + for (const k in node.#children) if (k.length > 1 && k !== ".*" && k !== "(?:|/.*)") throw PATH_ERROR; + nextNode = node.#children[token] = new Node(); + } + } + node = nextNode; + } + if (node.#index !== void 0) throw PATH_ERROR; + node.#index = isStatic ? -1 : index; + } + buildRegExpStr() { + const strList = Object.keys(this.#children).sort(compareKey).map((k) => { + const c = this.#children[k]; + const childStr = c.buildRegExpStr(); + return childStr === "" ? "" : (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + childStr; + }).filter(Boolean); + if (typeof this.#index === "number" && this.#index !== -1) strList.unshift(`#${this.#index}`); + if (strList.length === 0) return ""; + if (strList.length === 1) return strList[0]; + return "(?:" + strList.join("|") + ")"; + } +}; +//#endregion +export { LABEL_REG_EXP_STR, Node, ONLY_WILDCARD_REG_EXP_STR, PATH_ERROR, TAIL_WILDCARD_REG_EXP_STR }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js new file mode 100644 index 000000000..c9a8c1314 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js @@ -0,0 +1,94 @@ +import "../../router.js"; +import { emptyParam, match } from "./matcher.js"; +import { RegExpRouter } from "./router.js"; +//#region src/router/reg-exp-router/prepared-router.ts +var PreparedRegExpRouter = class { + name = "PreparedRegExpRouter"; + #matchers; + #relocateMap; + constructor(matchers, relocateMap) { + this.#matchers = matchers; + this.#relocateMap = relocateMap; + } + #addWildcard(method, handlerData) { + const matcher = this.#matchers[method]; + matcher[1].forEach((list) => list && list.push(handlerData)); + Object.values(matcher[2]).forEach((list) => list[0].push(handlerData)); + } + #addPath(method, path, handler, indexes, map) { + const matcher = this.#matchers[method]; + if (!map) matcher[2][path][0].push([handler, {}]); + else indexes.forEach((index) => { + if (typeof index === "number") matcher[1][index].push([handler, map]); + else matcher[2][index || path][0].push([handler, map]); + }); + } + add(method, path, handler) { + if (!this.#matchers[method]) { + const all = this.#matchers["ALL"]; + const staticMap = {}; + for (const key in all[2]) staticMap[key] = [all[2][key][0].slice(), emptyParam]; + this.#matchers[method] = [ + all[0], + all[1].map((list) => Array.isArray(list) ? list.slice() : 0), + staticMap + ]; + } + if (path === "/*" || path === "*") { + const handlerData = [handler, {}]; + if (method === "ALL") for (const m in this.#matchers) this.#addWildcard(m, handlerData); + else this.#addWildcard(method, handlerData); + return; + } + const data = this.#relocateMap[path]; + if (!data) throw new Error(`Path ${path} is not registered`); + for (const [indexes, map] of data) if (method === "ALL") for (const m in this.#matchers) this.#addPath(m, path, handler, indexes, map); + else this.#addPath(method, path, handler, indexes, map); + } + buildAllMatchers() { + return this.#matchers; + } + match = match; +}; +const buildInitParams = ({ paths }) => { + const RegExpRouterWithMatcherExport = class extends RegExpRouter { + buildAndExportAllMatchers() { + return this.buildAllMatchers(); + } + }; + const router = new RegExpRouterWithMatcherExport(); + for (const path of paths) router.add("ALL", path, path); + const matchers = router.buildAndExportAllMatchers(); + const all = matchers["ALL"]; + const relocateMap = {}; + for (const path of paths) { + if (path === "/*" || path === "*") continue; + all[1].forEach((list, i) => { + list.forEach(([p, map]) => { + if (p === path) { + if (relocateMap[path]) relocateMap[path][0][1] = { + ...relocateMap[path][0][1], + ...map + }; + else relocateMap[path] = [[[], map]]; + if (relocateMap[path][0][0].findIndex((j) => j === i) === -1) relocateMap[path][0][0].push(i); + } + }); + }); + for (const path2 in all[2]) all[2][path2][0].forEach(([p]) => { + if (p === path) { + relocateMap[path] ||= [[[]]]; + const value = path2 === path ? "" : path2; + if (relocateMap[path][0][0].findIndex((v) => v === value) === -1) relocateMap[path][0][0].push(value); + } + }); + } + for (let i = 0, len = all[1].length; i < len; i++) all[1][i] = all[1][i] ? [] : 0; + for (const path in all[2]) all[2][path][0] = []; + return [matchers, relocateMap]; +}; +const serializeInitParams = ([matchers, relocateMap]) => { + return `[${JSON.stringify(matchers, (_, value) => value instanceof RegExp ? `##${value.toString()}##` : value).replace(/"##(.+?)##"/g, (_, str) => str.replace(/\\\\/g, "\\"))},${JSON.stringify(relocateMap)}]`; +}; +//#endregion +export { PreparedRegExpRouter, buildInitParams, serializeInitParams }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js new file mode 100644 index 000000000..21ef235d1 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js @@ -0,0 +1,101 @@ +import { checkOptionalParameter } from "../../utils/url.js"; +import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from "../../router.js"; +import { createNullObject } from "../utils.js"; +import { emptyParam, match } from "./matcher.js"; +import { LABEL_REG_EXP_STR, PATH_ERROR, TAIL_WILDCARD_REG_EXP_STR } from "./node.js"; +import { Trie } from "./trie.js"; +//#region src/router/reg-exp-router/router.ts +let wildcardRegExpCache = createNullObject(); +function buildWildcardRegExp(path) { + return wildcardRegExpCache[path] ??= new RegExp(`^${path.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match, metaChar) => metaChar ? `\\${metaChar}` : match === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`); +} +function findMiddleware(middleware, path) { + for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(path)) return [...middleware[k]]; +} +var RegExpRouter = class { + name = "RegExpRouter"; + #middleware; + #routes; + #tries; + constructor() { + this.#middleware = { ["ALL"]: createNullObject() }; + this.#routes = { ["ALL"]: createNullObject() }; + this.#tries = { ["ALL"]: new Trie() }; + } + #insertPath(method, path) { + try { + this.#tries[method].insert(path, !/\*|\/:/.test(path)); + } catch (e) { + throw e === PATH_ERROR ? new UnsupportedPathError(path) : e; + } + } + add(method, path, handler) { + const middleware = this.#middleware; + const routes = this.#routes; + if (!middleware) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT); + if (!middleware[method]) { + this.#tries[method] = new Trie(); + for (const handlerMap of [middleware, routes]) { + handlerMap[method] = createNullObject(); + for (const p in handlerMap["ALL"]) { + handlerMap[method][p] = [...handlerMap["ALL"][p]]; + this.#insertPath(method, p); + } + } + } + if (path === "/*") path = "*"; + const methods = method === "ALL" ? Object.keys(middleware) : [method]; + if (/\*$/.test(path)) { + const re = buildWildcardRegExp(path); + for (const m of methods) if (!middleware[m][path]) { + this.#insertPath(m, path); + middleware[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || []; + } + for (const handlerMap of [middleware, routes]) for (const m of methods) for (const p in handlerMap[m]) re.test(p) && handlerMap[m][p].push([handler, path]); + return; + } + const paths = checkOptionalParameter(path) || [path]; + for (const path of paths) for (const m of methods) { + if (!routes[m][path]) { + this.#insertPath(m, path); + routes[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || []; + } + routes[m][path].push([handler, path]); + } + } + match = match; + buildAllMatchers() { + const matchers = createNullObject(); + for (const method of Object.keys(this.#routes)) matchers[method] = this.#buildMatcher(method); + this.#middleware = this.#routes = this.#tries = void 0; + wildcardRegExpCache = createNullObject(); + return matchers; + } + #buildMatcher(method) { + const middleware = this.#middleware[method]; + const routes = this.#routes[method]; + const trie = this.#tries[method]; + const staticMap = createNullObject(); + const handlerData = []; + const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp(); + for (const r of [middleware, routes]) for (const path in r) { + const handlers = r[path]; + const pathData = trie.paths[path]; + if (!pathData) { + staticMap[path] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam]; + continue; + } + handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [h, trie.paths[handlerPath][1].reduceRight((map, [key], i) => { + map[key] = paramReplacementMap[pathData[1][i][1]]; + return map; + }, createNullObject())]); + } + return [ + regexp, + indexReplacementMap.map((i) => handlerData[i]), + staticMap + ]; + } +}; +//#endregion +export { RegExpRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js new file mode 100644 index 000000000..a995678c0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js @@ -0,0 +1,73 @@ +import { createNullObject } from "../utils.js"; +import { Node } from "./node.js"; +//#region src/router/reg-exp-router/trie.ts +var Trie = class { + #context = { varIndex: 0 }; + #root = new Node(); + #index = 0; + paths = createNullObject(); + insert(path, isStatic) { + if (isStatic) { + this.#root.insert(path.split(""), 0, [], this.#context, true); + return; + } + const paramAssoc = []; + const groups = []; + let markedPath = path; + for (let i = 0;;) { + let replaced = false; + markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => { + const mark = `@\\${i}`; + groups[i] = [mark, m]; + i++; + replaced = true; + return mark; + }); + if (!replaced) break; + } + /** + * - pattern (:label, :label{0-9]+}, ...) + * - /* wildcard + * - character + */ + const tokens = markedPath.match(/(?::[^\/]+)|(?:\/\*$)|./g) || []; + for (let i = groups.length - 1; i >= 0; i--) { + const [mark] = groups[i]; + for (let j = tokens.length - 1; j >= 0; j--) if (tokens[j].indexOf(mark) !== -1) { + tokens[j] = tokens[j].replace(mark, groups[i][1]); + break; + } + } + this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false); + this.paths[path] = [this.#index++, paramAssoc]; + } + buildRegExp() { + let regexp = this.#root.buildRegExpStr(); + if (regexp === "") return [ + /^$/, + [], + [] + ]; + let captureIndex = 0; + const indexReplacementMap = []; + const paramReplacementMap = []; + regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => { + if (handlerIndex !== void 0) { + indexReplacementMap[++captureIndex] = Number(handlerIndex); + return "$()"; + } + if (paramIndex !== void 0) { + paramReplacementMap[Number(paramIndex)] = ++captureIndex; + return ""; + } + return ""; + }); + return [ + new RegExp(`^${regexp}`), + indexReplacementMap, + paramReplacementMap + ]; + } +}; +//#endregion +export { Trie }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js new file mode 100644 index 000000000..a115ab938 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js @@ -0,0 +1,49 @@ +import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from "../../router.js"; +//#region src/router/smart-router/router.ts +var SmartRouter = class { + name = "SmartRouter"; + #routers = []; + #routes = []; + constructor(init) { + this.#routers = init.routers; + } + add(method, path, handler) { + if (!this.#routes) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT); + this.#routes.push([ + method, + path, + handler + ]); + } + match(method, path) { + if (!this.#routes) throw new Error("Fatal error"); + const routers = this.#routers; + const routes = this.#routes; + const len = routers.length; + let i = 0; + let res; + for (; i < len; i++) { + const router = routers[i]; + try { + for (let i = 0, len = routes.length; i < len; i++) router.add(...routes[i]); + res = router.match(method, path); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + this.match = router.match.bind(router); + this.#routers = [router]; + this.#routes = void 0; + break; + } + if (i === len) throw new Error("Fatal error"); + this.name = `SmartRouter + ${this.activeRouter.name}`; + return res; + } + get activeRouter() { + if (this.#routes || this.#routers.length !== 1) throw new Error("No active router has been determined yet."); + return this.#routers[0]; + } +}; +//#endregion +export { SmartRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js new file mode 100644 index 000000000..8ea5b3150 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js @@ -0,0 +1,2 @@ +import { TrieRouter } from "./router.js"; +export { TrieRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js new file mode 100644 index 000000000..c33c7af1e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js @@ -0,0 +1,134 @@ +import { getPattern, splitPath, splitRoutingPath } from "../../utils/url.js"; +import "../../router.js"; +import { createNullObject } from "../utils.js"; +//#region src/router/trie-router/node.ts +const emptyParams = createNullObject(); +let order = 0; +var Node = class Node { + #methods = []; + #children = createNullObject(); + #patterns = []; + #pattern; + #params = emptyParams; + insert(method, path, handler) { + let curNode = this; + const parts = splitRoutingPath(path); + const possibleKeys = /* @__PURE__ */ new Set(); + let i = 0; + for (const p of parts) { + const nextP = parts[++i]; + const pattern = getPattern(p, nextP) || (nextP === void 0 && p && p.indexOf("*") === p.length - 1 ? p : null); + const isParam = Array.isArray(pattern); + const key = isParam ? pattern[0] : pattern || p; + const child = curNode.#children[key] ||= new Node(); + if (pattern && !child.#pattern) { + child.#pattern = pattern; + curNode.#patterns.push(child); + } + curNode = child; + if (isParam) possibleKeys.add(pattern[1]); + } + curNode.#methods.push({ [method]: { + handler, + possibleKeys: [...possibleKeys], + score: ++order + } }); + } + #pushHandlerSets(handlerSets, node, method, nodeParams, params) { + for (let i = 0, len = node.#methods.length; i < len; i++) { + const m = node.#methods[i]; + const handlerSet = m[method] || m["ALL"]; + if (handlerSet) { + handlerSet.params = createNullObject(); + handlerSets.push(handlerSet); + for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) { + const key = handlerSet.possibleKeys[i]; + handlerSet.params[key] = params?.[key] && !i ? params[key] : nodeParams[key] ?? params?.[key]; + } + } + } + } + search(method, path) { + const handlerSets = []; + this.#params = emptyParams; + let curNodes = [this]; + const parts = splitPath(path); + const curNodesQueue = []; + const len = parts.length; + let partOffsets = null; + for (let i = 0; i < len; i++) { + const part = parts[i]; + const isLast = i === len - 1; + const tempNodes = []; + for (let j = 0, len2 = curNodes.length; j < len2; j++) { + const node = curNodes[j]; + const nextNode = node.#children[part]; + if (nextNode) { + nextNode.#params = node.#params; + if (isLast) { + if (nextNode.#children["*"]) this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params); + this.#pushHandlerSets(handlerSets, nextNode, method, node.#params); + } else tempNodes.push(nextNode); + } + for (const child of node.#patterns) { + const pattern = child.#pattern; + const params = node.#params === emptyParams ? {} : { ...node.#params }; + if (typeof pattern === "string") { + if (pattern === "*" || part.startsWith(pattern.slice(0, -1))) { + this.#pushHandlerSets(handlerSets, child, method, node.#params); + if (pattern === "*") { + child.#params = params; + tempNodes.push(child); + } + } + continue; + } + const [, name, matcher] = pattern; + if (!part && matcher === true) continue; + if (matcher !== true) { + if (!partOffsets) { + partOffsets = []; + let offset = path[0] === "/" ? 1 : 0; + for (let p = 0; p < len; p++) { + partOffsets[p] = offset; + offset += parts[p].length + 1; + } + } + const restPathString = path.slice(partOffsets[i]); + const m = matcher.exec(restPathString); + if (m) { + params[name] = m[0]; + this.#pushHandlerSets(handlerSets, child, method, node.#params, params); + if (m[0].length === restPathString.length && child.#children["*"]) this.#pushHandlerSets(handlerSets, child.#children["*"], method, node.#params, params); + for (const _ in child.#children) { + child.#params = params; + const componentCount = m[0].match(/\//g)?.length ?? 0; + (curNodesQueue[componentCount] ||= []).push(child); + break; + } + continue; + } + } + if (matcher === true || matcher.test(part)) { + params[name] = part; + if (isLast) { + this.#pushHandlerSets(handlerSets, child, method, params, node.#params); + if (child.#children["*"]) this.#pushHandlerSets(handlerSets, child.#children["*"], method, params, node.#params); + } else { + child.#params = params; + tempNodes.push(child); + } + } + } + } + const shifted = curNodesQueue.shift(); + curNodes = shifted ? tempNodes.concat(shifted) : tempNodes; + } + if (handlerSets[1]) handlerSets.sort((a, b) => { + return a.score - b.score; + }); + return [handlerSets.map(({ handler, params }) => [handler, params])]; + } +}; +//#endregion +export { Node }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js new file mode 100644 index 000000000..96f4c9351 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js @@ -0,0 +1,15 @@ +import { checkOptionalParameter } from "../../utils/url.js"; +import { Node } from "./node.js"; +//#region src/router/trie-router/router.ts +var TrieRouter = class { + name = "TrieRouter"; + #node = new Node(); + add(method, path, handler) { + for (const result of checkOptionalParameter(path) || [path]) this.#node.insert(method, result, handler); + } + match(method, path) { + return this.#node.search(method, path); + } +}; +//#endregion +export { TrieRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/utils.js b/tests/fixtures/npm/node_modules/hono/dist/router/utils.js new file mode 100644 index 000000000..3f1e9d328 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/utils.js @@ -0,0 +1,4 @@ +//#region src/router/utils.ts +const createNullObject = () => Object.create(null); +//#endregion +export { createNullObject }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts new file mode 100644 index 000000000..81dc47313 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts @@ -0,0 +1,204 @@ +import { METHODS, METHOD_NAME_ALL_LOWERCASE } from "../router.js"; +import { HonoBase as Hono } from "../hono-base.js"; +import { StatusCode, SuccessStatusCode } from "../utils/http-status.js"; +import { HasRequiredKeys } from "../utils/types.js"; +import { Endpoint, ExtractSchema, KnownResponseFormat, ResponseFormat, Schema } from "../types.js"; +import { Hono as Hono$1 } from "../hono.js"; +//#region src/client/types.d.ts +/** + * Type representing the '$all' method name + */ +type MethodNameAll = `$${typeof METHOD_NAME_ALL_LOWERCASE}`; +/** + * Type representing all standard HTTP methods prefixed with '$' + * e.g., '$get' | '$post' | '$put' | '$delete' | '$options' | '$patch' | '$query' + */ +type StandardMethods = `$${(typeof METHODS)[number]}`; +/** + * Expands '$all' into all standard HTTP methods. + * If the schema contains '$all', it creates a type where all standard HTTP methods + * point to the same endpoint definition as '$all', while removing '$all' itself. + */ +type ExpandAllMethod = MethodNameAll extends keyof S ? { [M in StandardMethods]: S[MethodNameAll]; } & Omit : S; +type HonoRequest = (typeof Hono$1.prototype)['request']; +export type BuildSearchParamsFn = (query: Record) => URLSearchParams; +export type ClientRequestOptions = { + fetch?: typeof fetch | HonoRequest; + webSocket?: (...args: ConstructorParameters) => WebSocket; + /** + * Standard `RequestInit`, caution that this take highest priority + * and could be used to overwrite things that Hono sets for you, like `body | method | headers`. + * + * If you want to add some headers, use in `headers` instead of `init` + */ + init?: RequestInit; + /** + * Custom function to serialize query parameters into URLSearchParams. + * By default, arrays are serialized as multiple parameters with the same key (e.g., `key=a&key=b`). + * You can provide a custom function to change this behavior, for example to use bracket notation (e.g., `key[]=a&key[]=b`). + * + * @example + * ```ts + * const client = hc('http://localhost', { + * buildSearchParams: (query) => { + * return new URLSearchParams(qs.stringify(query)) + * } + * }) + * ``` + */ + buildSearchParams?: BuildSearchParamsFn; +} & (keyof T extends never ? { + headers?: Record | (() => Record | Promise>); +} : { + headers: T | (() => T | Promise); +}); +export type ClientRequest = { [M in keyof ExpandAllMethod]: ExpandAllMethod[M] extends Endpoint & { + input: infer R; +} ? R extends object ? HasRequiredKeys extends true ? (args: R, options?: ClientRequestOptions) => Promise[M]>> : (args?: R, options?: ClientRequestOptions) => Promise[M]>> : never : never; } & { + $url: (arg?: Arg) => HonoURL; + $path: (arg?: Arg) => BuildPath; +} & (S['$get'] extends { + outputFormat: 'ws'; +} ? S['$get'] extends { + input: infer I; +} ? { + $ws: (args?: I) => WebSocket; +} : {} : {}); +type ClientResponseOfEndpoint = T extends { + output: infer O; + outputFormat: infer F; + status: infer S; +} ? ClientResponse : never; +export interface ClientResponse { + readonly body: ReadableStream | null; + readonly bodyUsed: boolean; + ok: U extends SuccessStatusCode ? true : U extends Exclude ? false : boolean; + redirected: boolean; + status: U; + statusText: string; + type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect'; + headers: Headers; + url: string; + redirect(url: string, status: number): Response; + clone(): Response; + bytes(): Promise>; + json(): F extends 'text' ? Promise : F extends 'json' ? Promise : Promise; + text(): F extends 'text' ? (T extends string ? Promise : Promise) : Promise; + blob(): Promise; + formData(): Promise; + arrayBuffer(): Promise; +} +type BuildSearch = Arg extends { [K in Key]: infer Query; } ? IsEmptyObject extends true ? '' : `?${string}` : ''; +type BuildPathname

= Arg extends { + param: infer Param; +} ? `${ApplyParam, Param>}` : `/${TrimStartSlash

}`; +type BuildPath

= `${BuildPathname}${BuildSearch}`; +type BuildTypedURL = TypedURL<`${Protocol}:`, Host, Port, BuildPathname, BuildSearch>; +type HonoURL = IsLiteral extends true ? TrimEndSlash extends `${infer Protocol}://${infer Rest}` ? Rest extends `${infer Hostname}/${infer P}` ? ParseHostName extends [infer Host extends string, infer Port extends string] ? BuildTypedURL : never : ParseHostName extends [infer Host extends string, infer Port extends string] ? BuildTypedURL : never : URL : URL; +type ParseHostName = T extends `${infer Host}:${infer Port}` ? [Host, Port] : [T, '']; +type TrimStartSlash = T extends `/${infer R}` ? TrimStartSlash : T; +type TrimEndSlash = T extends `${infer R}/` ? TrimEndSlash : T; +type IsLiteral = [T] extends [never] ? false : string extends T ? false : true; +type ApplyParam = Path extends `${infer Head}/${infer Rest}` ? Head extends `:${infer Param}` ? P extends Record ? IsLiteral extends true ? ApplyParam : ApplyParam : ApplyParam : ApplyParam : Path extends `:${infer Param}` ? P extends Record ? IsLiteral extends true ? `${Result}/${Value & string}` : `${Result}/${Path}` : `${Result}/${Path}` : `${Result}/${Path}`; +type IsEmptyObject = keyof T extends never ? true : false; +export interface TypedURL extends URL { + protocol: Protocol; + hostname: Hostname; + port: Port; + host: Port extends '' ? Hostname : `${Hostname}:${Port}`; + origin: `${Protocol}//${Hostname}${Port extends '' ? '' : `:${Port}`}`; + pathname: Pathname; + search: Search; + href: `${Protocol}//${Hostname}${Port extends '' ? '' : `:${Port}`}${Pathname}${Search}`; +} +export interface Response extends ClientResponse {} +export type Fetch = (args?: InferRequestType, opt?: ClientRequestOptions) => Promise>>; +type InferEndpointType = T extends ((args: infer R, options: any | undefined) => Promise) ? U extends ClientResponse ? { + input: NonNullable; + output: O; + outputFormat: F; + status: S; +} extends Endpoint ? { + input: NonNullable; + output: O; + outputFormat: F; + status: S; +} : never : never : never; +export type InferResponseType = InferResponseTypeFromEndpoint, U>; +type InferResponseTypeFromEndpoint = T extends { + output: infer O; + status: infer S; +} ? S extends U ? O : never : never; +export type InferRequestType = T extends ((args: infer R, options: any | undefined) => Promise>) ? NonNullable : never; +export type InferRequestOptionsType = T extends ((args: any, options: infer R) => Promise>) ? NonNullable : never; +/** + * Filter a ClientResponse type so it only includes responses of specific status codes. + */ +export type FilterClientResponseByStatusCode, U extends number = StatusCode> = T extends ClientResponse ? RC extends U ? ClientResponse : never : never; +type PathToChain = Path extends `/${infer P}` ? PathToChain : Path extends `${infer P}/${infer R}` ? { [K in P]: PathToChain; } : { [K in Path extends '' ? 'index' : Path]: ClientRequest ? E[Original] : never>; }; +export type Client = T extends Hono ? S extends Record ? K extends string ? PathToChain : never : never : never; +export type Callback = (opts: CallbackOptions) => unknown; +interface CallbackOptions { + path: string[]; + args: any[]; +} +export type ObjectType = { + [key: string]: T; +}; +type GlobalResponseDefinition = { [S in StatusCode]?: { [F in KnownResponseFormat]?: unknown; }; }; +type ToEndpoints = { [S in keyof Def & StatusCode]: { [F in keyof Def[S] & KnownResponseFormat]: Omit & { + output: Def[S][F]; + status: S; + outputFormat: F; +}; }[keyof Def[S] & KnownResponseFormat]; }[keyof Def & StatusCode]; +type ModRoute = R extends Endpoint ? R | ToEndpoints : R; +type ModSchema = { [K in keyof D]: { [M in keyof D[K]]: ModRoute; }; }; +export type ApplyGlobalResponse = App extends Hono ? ModSchema, Def> extends (infer S extends Schema) ? Hono$1 : never : never; +type PickRoute = R extends Endpoint ? R extends { + status: U; +} ? R : never : R; +type PickSchema = { [K in keyof D]: { [M in keyof D[K]]: PickRoute; }; }; +/** + * Keep only specific status code responses from all routes of an app. + * Useful when error responses are handled centrally (e.g., via custom fetch) + * and you want the client to only expose success response types. + * + * @example + * ```ts + * type AppSuccessOnly = PickResponseByStatusCode + * const client = hc('http://localhost') + * ``` + */ +export type PickResponseByStatusCode = App extends Hono ? PickSchema, U> extends (infer S extends Schema) ? Hono$1 : never : never; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts new file mode 100644 index 000000000..163c86b8c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts @@ -0,0 +1,459 @@ +import { Result } from "./router.js"; +import { ResponseHeader } from "./utils/headers.js"; +import { ContentfulStatusCode, RedirectStatusCode, StatusCode } from "./utils/http-status.js"; +import { InvalidJSONValue, IsAny, JSONParsed, JSONValue } from "./utils/types.js"; +import { Env, FetchEventLike, H, Input, NotFoundHandler, RouterRoute, TypedResponse } from "./types.js"; +import { HonoRequest } from "./request.js"; +import { BaseMime } from "./utils/mime.js"; +//#region src/context.d.ts +type HeaderRecord = Record<'Content-Type', BaseMime> | Record | Record; +/** + * Data type can be a string, ArrayBuffer, Blob, Uint8Array (buffer), or ReadableStream. + */ +export type Data = string | ArrayBuffer | Blob | ReadableStream | Uint8Array; +/** + * Interface for the execution context in a web worker or similar environment. + */ +export interface ExecutionContext { + /** + * Extends the lifetime of the event callback until the promise is settled. + * + * @param promise - A promise to wait for. + */ + waitUntil(promise: Promise): void; + /** + * Allows the event to be passed through to subsequent event listeners. + */ + passThroughOnException(): void; + /** + * For compatibility with Wrangler 4.x. + */ + props: any; + /** + * For compatibility with Wrangler 4.x. + */ + exports?: any; +} +/** + * Interface for context variable mapping. + */ +export interface ContextVariableMap {} +/** + * Interface for context renderer. + */ +export interface ContextRenderer {} +/** + * Interface representing a renderer for content. + * + * @interface DefaultRenderer + * @param {string | Promise} content - The content to be rendered, which can be either a string or a Promise resolving to a string. + * @returns {Response | Promise} - The response after rendering the content, which can be either a Response or a Promise resolving to a Response. + */ +interface DefaultRenderer { + (content: string | Promise): Response | Promise; +} +/** + * Renderer type which can either be a ContextRenderer or DefaultRenderer. + */ +export type Renderer = ContextRenderer extends Function ? ContextRenderer : DefaultRenderer; +/** + * Extracts the props for the renderer. + */ +export type PropsForRenderer = [...Required>] extends [unknown, infer Props] ? Props : unknown; +export type Layout> = (props: T) => any; +/** + * Interface for getting context variables. + * + * @template E - Environment type. + */ +interface Get { + (key: Key): E['Variables'][Key]; + (key: Key): ContextVariableMap[Key]; +} +/** + * Interface for setting context variables. + * + * @template E - Environment type. + */ +interface Set { + (key: Key, value: E['Variables'][Key]): void; + (key: Key, value: ContextVariableMap[Key]): void; +} +/** + * Interface for creating a new response. + */ +interface NewResponse { + (data: Data | null, status?: StatusCode, headers?: HeaderRecord): Response; + (data: Data | null, init?: ResponseOrInit): Response; +} +/** + * Interface for responding with a body. + */ +interface BodyRespond { + (data: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (data: T, init?: ResponseOrInit): Response & TypedResponse; + (data: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (data: T, init?: ResponseOrInit): Response & TypedResponse; +} +/** + * Interface for responding with text. + * + * @interface TextRespond + * @template T - The type of the text content. + * @template U - The type of the status code. + * + * @param {T} text - The text content to be included in the response. + * @param {U} [status] - An optional status code for the response. + * @param {HeaderRecord} [headers] - An optional record of headers to include in the response. + * + * @returns {Response & TypedResponse} - The response after rendering the text content, typed with the provided text and status code types. + */ +interface TextRespond { + (text: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (text: T, init?: ResponseOrInit): Response & TypedResponse; +} +/** + * Interface for responding with JSON. + * + * @interface JSONRespond + * @template T - The type of the JSON value or simplified unknown type. + * @template U - The type of the status code. + * + * @param {T} object - The JSON object to be included in the response. + * @param {U} [status] - An optional status code for the response. + * @param {HeaderRecord} [headers] - An optional record of headers to include in the response. + * + * @returns {JSONRespondReturn} - The response after rendering the JSON object, typed with the provided object and status code types. + */ +interface JSONRespond { + (object: T, status?: U, headers?: HeaderRecord): JSONRespondReturn; + (object: T, init?: ResponseOrInit): JSONRespondReturn; +} +/** + * @template T - The type of the JSON value or simplified unknown type. + * @template U - The type of the status code. + * + * @returns {Response & TypedResponse, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types. + */ +type JSONRespondReturn = Response & TypedResponse, U, 'json'>; +/** + * Interface representing a function that responds with HTML content. + * + * @param html - The HTML content to respond with, which can be a string or a Promise that resolves to a string. + * @param status - (Optional) The HTTP status code for the response. + * @param headers - (Optional) A record of headers to include in the response. + * @param init - (Optional) The response initialization object. + * + * @returns A Response object or a Promise that resolves to a Response object. + */ +interface HTMLRespond { + >(html: T, status?: ContentfulStatusCode, headers?: HeaderRecord): T extends string ? Response : Promise; + >(html: T, init?: ResponseOrInit): T extends string ? Response : Promise; +} +/** + * Options for configuring the context. + * + * @template E - Environment type. + */ +type ContextOptions = { + /** + * Bindings for the environment. + */ + env: E['Bindings']; + /** + * Execution context for the request. + */ + executionCtx?: FetchEventLike | ExecutionContext | undefined; + /** + * Handler for not found responses. + */ + notFoundHandler?: NotFoundHandler; + matchResult?: Result<[H, RouterRoute]>; + path?: string; +}; +interface SetHeadersOptions { + append?: boolean; +} +interface SetHeaders { + (name: 'Content-Type', value?: BaseMime, options?: SetHeadersOptions): void; + (name: ResponseHeader, value?: string, options?: SetHeadersOptions): void; + (name: string, value?: string, options?: SetHeadersOptions): void; +} +type ResponseHeadersInit = [string, string][] | Record<'Content-Type', BaseMime> | Record | Record | Headers; +interface ResponseInit { + headers?: ResponseHeadersInit; + status?: T; + statusText?: string; +} +type ResponseOrInit = ResponseInit | Response; +export declare const TEXT_PLAIN = "text/plain; charset=UTF-8"; +export declare class Context { + + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * + * @see {@link https://hono.dev/docs/api/context#env} + * + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + */ + env: E['Bindings']; + finalized: boolean; + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * + * @see {@link https://hono.dev/docs/api/context#error} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + */ + error: Error | undefined; + /** + * Creates an instance of the Context class. + * + * @param req - The Request object. + * @param options - Optional configuration options for the context. + */ + constructor(req: Request, options?: ContextOptions); + /** + * `.req` is the instance of {@link HonoRequest}. + */ + get req(): HonoRequest; + /** + * @see {@link https://hono.dev/docs/api/context#event} + * The FetchEvent associated with the current request. + * + * @throws Will throw an error if the context does not have a FetchEvent. + */ + get event(): FetchEventLike; + /** + * @see {@link https://hono.dev/docs/api/context#executionctx} + * The ExecutionContext associated with the current request. + * + * @throws Will throw an error if the context does not have an ExecutionContext. + */ + get executionCtx(): ExecutionContext; + /** + * @see {@link https://hono.dev/docs/api/context#res} + * The Response object for the current request. + */ + get res(): Response; + /** + * Sets the Response object for the current request. + * + * @param _res - The Response object to set. + */ + set res(_res: Response | undefined); + /** + * `.render()` can create a response within a layout. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + */ + render: Renderer; + /** + * Sets the layout for the response. + * + * @param layout - The layout to set. + * @returns The layout function. + */ + setLayout: (layout: Layout) => Layout; + /** + * Gets the current layout for the response. + * + * @returns The current layout function. + */ + getLayout: () => Layout | undefined; + /** + * `.setRenderer()` can set the layout in the custom middleware. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + */ + setRenderer: (renderer: Renderer) => void; + /** + * `.header()` can set headers. + * + * @see {@link https://hono.dev/docs/api/context#header} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * // Append multiple headers using the append option (e.g. Vary) + * c.header('Vary', 'Accept-Encoding', { append: true }) + * c.header('Vary', 'User-Agent', { append: true }) + * + * return c.body('Thank you for coming') + * }) + * ``` + */ + header: SetHeaders; + status: (status: StatusCode) => void; + /** + * `.set()` can set the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is hot!!') + * await next() + * }) + * ``` + */ + set: Set extends true ? { + Variables: ContextVariableMap & Record; + } : E>; + /** + * `.get()` can use the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + */ + get: Get extends true ? { + Variables: ContextVariableMap & Record; + } : E>; + /** + * `.var` can access the value of a variable. + * + * @see {@link https://hono.dev/docs/api/context#var} + * + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + */ + get var(): Readonly extends true ? Record : E['Variables'])>; + newResponse: NewResponse; + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * + * @see {@link https://hono.dev/docs/api/context#body} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + */ + body: BodyRespond; + /** + * `.text()` can render text as `Content-Type:text/plain`. + * + * @see {@link https://hono.dev/docs/api/context#text} + * + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + */ + text: TextRespond; + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * + * @see {@link https://hono.dev/docs/api/context#json} + * + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + */ + json: JSONRespond; + html: HTMLRespond; + /** + * `.redirect()` can Redirect, default status code is 302. + * + * @see {@link https://hono.dev/docs/api/context#redirect} + * + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + */ + redirect: (location: string | URL, status?: T) => Response & TypedResponse; + /** + * `.notFound()` can return the Not Found Response. + * + * @see {@link https://hono.dev/docs/api/context#notfound} + * + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + */ + notFound: () => ReturnType; +} +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts new file mode 100644 index 000000000..85f57dfc6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts @@ -0,0 +1,221 @@ +import { Router } from "./router.js"; +import { Env, ErrorHandler, H, HandlerInterface, MergePath, MergeSchemaPath, MiddlewareHandlerInterface, NotFoundHandler, OnHandlerInterface, RouterRoute, Schema } from "./types.js"; +import { Context, ExecutionContext } from "./context.js"; +//#region src/hono-base.d.ts +type GetPath = (request: Request, options?: { + env?: E['Bindings']; +}) => string; +export type HonoOptions = { + /** + * `strict` option specifies whether to distinguish whether the last path is a directory or not. + * + * @see {@link https://hono.dev/docs/api/hono#strict-mode} + * + * @default true + */ + strict?: boolean; + /** + * `router` option specifies which router to use. + * + * @see {@link https://hono.dev/docs/api/hono#router-option} + * + * @example + * ```ts + * const app = new Hono({ router: new RegExpRouter() }) + * ``` + */ + router?: Router<[H, RouterRoute]>; + /** + * `getPath` can handle the host header value. + * + * @see {@link https://hono.dev/docs/api/routing#routing-with-host-header-value} + * + * @example + * ```ts + * const app = new Hono({ + * getPath: (req) => + * '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/[^?]*)/, '$1'), + * }) + * + * app.get('/www1.example.com/hello', () => c.text('hello www1')) + * + * // A following request will match the route: + * // new Request('http://www1.example.com/hello', { + * // headers: { host: 'www1.example.com' }, + * // }) + * ``` + */ + getPath?: GetPath; +}; +type MountOptionHandler = (c: Context) => unknown; +type MountReplaceRequest = (originalRequest: Request) => Request; +type MountOptions = MountOptionHandler | { + optionHandler?: MountOptionHandler; + replaceRequest?: MountReplaceRequest | false; +}; +declare class Hono { + + get: HandlerInterface; + post: HandlerInterface; + put: HandlerInterface; + delete: HandlerInterface; + options: HandlerInterface; + patch: HandlerInterface; + query: HandlerInterface; + all: HandlerInterface; + on: OnHandlerInterface; + use: MiddlewareHandlerInterface; + router: Router<[H, RouterRoute]>; + readonly getPath: GetPath; + private _basePath; + routes: RouterRoute[]; + constructor(options?: HonoOptions); + private errorHandler; + /** + * `.route()` allows grouping other Hono instance in routes. + * + * @see {@link https://hono.dev/docs/api/routing#grouping} + * + * @param {string} path - base Path + * @param {Hono} app - other Hono instance + * @returns {Hono} routed Hono instance + * + * @example + * ```ts + * const app = new Hono() + * const app2 = new Hono() + * + * app2.get("/user", (c) => c.text("user")) + * app.route("/api", app2) // GET /api/user + * ``` + */ + route(path: SubPath, app: Hono): Hono> | S, BasePath, CurrentPath>; + /** + * `.basePath()` allows base paths to be specified. + * + * @see {@link https://hono.dev/docs/api/routing#base-path} + * + * @param {string} path - base Path + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + */ + basePath(path: SubPath): Hono, MergePath>; + /** + * `.onError()` handles an error and returns a customized Response. + * + * @see {@link https://hono.dev/docs/api/hono#error-handling} + * + * @param {ErrorHandler} handler - request Handler for error + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ + onError: (handler: ErrorHandler) => Hono; + /** + * `.notFound()` allows you to customize a Not Found Response. + * + * @see {@link https://hono.dev/docs/api/hono#not-found} + * + * @param {NotFoundHandler} handler - request handler for not-found + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + */ + notFound: (handler: NotFoundHandler) => Hono; + /** + * `.mount()` allows you to mount applications built with other frameworks into your Hono application. + * + * @deprecated Use `mount()` from `hono/mount` instead. `.mount()` will be removed in v5. + * + * @see {@link https://hono.dev/docs/api/hono#mount} + * + * @param {string} path - base Path + * @param {Function} applicationHandler - other Request Handler + * @param {MountOptions} [options] - options of `.mount()` + * @returns {Hono} mounted Hono instance + * + * @example + * ```ts + * import { Router as IttyRouter } from 'itty-router' + * import { Hono } from 'hono' + * // Create itty-router application + * const ittyRouter = IttyRouter() + * // GET /itty-router/hello + * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) + * + * const app = new Hono() + * app.mount('/itty-router', ittyRouter.handle) + * ``` + * + * @example + * ```ts + * const app = new Hono() + * // Send the request to another application without modification. + * app.mount('/app', anotherApp, { + * replaceRequest: (req) => req, + * }) + * ``` + */ + mount(path: string, applicationHandler: (request: Request, ...args: any) => Response | Promise, options?: MountOptions): Hono; + /** + * `.fetch()` will be entry point of your app. + * + * @see {@link https://hono.dev/docs/api/hono#fetch} + * + * @param {Request} request - request Object of request + * @param {Env} env - env Object + * @param {ExecutionContext} executionCtx - context of execution + * @returns {Response | Promise} response of request + * + */ + fetch: (request: Request, env?: E['Bindings'] | {}, executionCtx?: ExecutionContext) => Response | Promise; + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/docs/api/hono#request + */ + request: (input: Request | string | URL, requestInit?: RequestInit, Env?: E["Bindings"] | {}, executionCtx?: ExecutionContext) => Response | Promise; + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @deprecated + * Use `fire` from `hono/service-worker` instead. + * ```ts + * import { Hono } from 'hono' + * import { fire } from 'hono/service-worker' + * + * const app = new Hono() + * // ... + * fire(app) + * ``` + * @see https://hono.dev/docs/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ + fire: () => void; +} +//#endregion +export { Hono as HonoBase }; +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts new file mode 100644 index 000000000..52ce78cec --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts @@ -0,0 +1,20 @@ +import { HonoBase as Hono$1, HonoOptions } from "./hono-base.js"; +import { BlankEnv, BlankSchema, Env, Schema } from "./types.js"; +//#region src/hono.d.ts +/** + * The Hono class extends the functionality of the HonoBase class. + * It sets up routing and allows for custom options to be passed. + * + * @template E - The environment type. + * @template S - The schema type. + * @template BasePath - The base path type. + */ +export declare class Hono extends Hono$1 { + /** + * Creates an instance of the Hono class. + * + * @param options - Optional configuration options for the Hono instance. + */ + constructor(options?: HonoOptions); +} +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts new file mode 100644 index 000000000..12bbd7aaf --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts @@ -0,0 +1,6 @@ +import { Env, ErrorHandler, Handler, Input, MiddlewareHandler, Next, NotFoundHandler, NotFoundResponse, Schema, ToSchema, TypedResponse, ValidationTargets } from "./types.js"; +import { HonoRequest } from "./request.js"; +import { Context, ContextRenderer, ContextVariableMap, ExecutionContext } from "./context.js"; +import { Hono } from "./hono.js"; +import { ClientRequestOptions, InferRequestType, InferResponseType } from "./client/types.js"; +export { type ClientRequestOptions, Context, type ContextRenderer, type ContextVariableMap, type Env, type ErrorHandler, type ExecutionContext, type Handler, Hono, type HonoRequest, type InferRequestType, type InferResponseType, type Input, type MiddlewareHandler, type Next, type NotFoundHandler, type NotFoundResponse, type Schema, type ToSchema, type TypedResponse, type ValidationTargets }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts new file mode 100644 index 000000000..03d14a385 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts @@ -0,0 +1,326 @@ +import { GET_MATCH_RESULT } from "./request/constants.js"; +import { Result } from "./router.js"; +import { CustomHeader, RequestHeader } from "./utils/headers.js"; +import { Simplify, UnionToIntersection } from "./utils/types.js"; +import { Input, InputToDataByTarget, ParamKeyToRecord, ParamKeys, RemoveQuestion, RouterRoute, ValidationTargets } from "./types.js"; +import { BodyData, ParseBodyOptions } from "./utils/body.js"; +//#region src/request.d.ts +type Body = { + json: any; + text: string; + arrayBuffer: ArrayBuffer; + blob: Blob; + formData: FormData; +}; +type BodyCache = Partial; +export declare class HonoRequest

{ + + /** + * `.raw` can get the raw Request object. + * + * @see {@link https://hono.dev/docs/api/request#raw} + * + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + */ + raw: Request; + routeIndex: number; + /** + * `.path` can get the pathname of the request. + * + * @see {@link https://hono.dev/docs/api/request#path} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + */ + path: string; + bodyCache: BodyCache; + constructor(request: Request, path?: string, matchResult?: Result<[unknown, RouterRoute]>); + /** + * `.req.param()` gets the path parameters. + * + * @see {@link https://hono.dev/docs/api/routing#path-parameter} + * + * @example + * ```ts + * const name = c.req.param('name') + * // or all parameters at once + * const { id, comment_id } = c.req.param() + * ``` + */ + param = ParamKeys

>(key: string extends P ? never : P2 extends `${infer _}?` ? never : P2): string; + param> = RemoveQuestion>>(key: P2): string | undefined; + param(key: string): string | undefined; + param(): Simplify>>>; + /** + * `.query()` can get querystring parameters. + * + * @see {@link https://hono.dev/docs/api/request#query} + * + * @example + * ```ts + * // Query params + * app.get('/search', (c) => { + * const query = c.req.query('q') + * }) + * + * // Get all params at once + * app.get('/search', (c) => { + * const { q, limit, offset } = c.req.query() + * }) + * ``` + */ + query(key: string): string | undefined; + query(): Record; + /** + * `.queries()` can get multiple querystring parameter values, e.g. /search?tags=A&tags=B + * + * @see {@link https://hono.dev/docs/api/request#queries} + * + * @example + * ```ts + * app.get('/search', (c) => { + * // tags will be string[] + * const tags = c.req.queries('tags') + * }) + * ``` + */ + queries(key: string): string[] | undefined; + queries(): Record; + /** + * `.header()` can get the request header value. + * + * @see {@link https://hono.dev/docs/api/request#header} + * + * @example + * ```ts + * app.get('/', (c) => { + * const userAgent = c.req.header('User-Agent') + * }) + * ``` + */ + header(name: RequestHeader): string | undefined; + header(name: string): string | undefined; + header(): Record; + /** + * `.parseBody()` can parse Request body of type `multipart/form-data` or `application/x-www-form-urlencoded` + * + * @see {@link https://hono.dev/docs/api/request#parsebody} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.parseBody() + * }) + * ``` + */ + parseBody, T extends BodyData>(options?: Options): Promise; + parseBody(options?: Partial): Promise; + /** + * `.json()` can parse Request body of type `application/json` + * + * @see {@link https://hono.dev/docs/api/request#json} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + */ + json(): Promise; + /** + * `.text()` can parse Request body of type `text/plain` + * + * @see {@link https://hono.dev/docs/api/request#text} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + */ + text(): Promise; + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * + * @see {@link https://hono.dev/docs/api/request#arraybuffer} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + */ + arrayBuffer(): Promise; + /** + * `.bytes()` parses the request body as a `Uint8Array`. + * + * @see {@link https://hono.dev/docs/api/request#bytes} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.bytes() + * }) + * ``` + */ + bytes(): Promise; + /** + * Parses the request body as a `Blob`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.blob(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#blob + */ + blob(): Promise; + /** + * Parses the request body as `FormData`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.formData(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#formdata + */ + formData(): Promise; + /** + * Adds validated data to the request. + * + * @param target - The target of the validation. + * @param data - The validated data to add. + */ + addValidatedData(target: keyof ValidationTargets, data: {}): void; + /** + * Gets validated data from the request. + * + * @param target - The target of the validation. + * @returns The validated data. + * + * @see https://hono.dev/docs/api/request#valid + */ + valid(target: T): InputToDataByTarget; + /** + * `.url` can get the request url strings. + * + * @see {@link https://hono.dev/docs/api/request#url} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + */ + get url(): string; + /** + * `.method` can get the method name of the request. + * + * @see {@link https://hono.dev/docs/api/request#method} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + */ + get method(): string; + get [GET_MATCH_RESULT](): Result<[unknown, RouterRoute]>; + /** + * `.matchedRoutes` can return a matched route in the handler + * + * @deprecated + * + * Use matchedRoutes helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#matchedroutes} + * + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + */ + get matchedRoutes(): RouterRoute[]; + /** + * `.routePath` can retrieve the path registered within the handler + * + * @deprecated + * + * Use routePath helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#routepath} + * + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + */ + get routePath(): string; +} +/** + * Clones a HonoRequest's underlying raw Request object. + * + * This utility handles both consumed and unconsumed request bodies: + * - If the request body hasn't been consumed, it uses the native `clone()` method + * - If the request body has been consumed, it reconstructs a new Request using cached body data + * + * This is particularly useful when you need to: + * - Process the same request body multiple times + * - Pass requests to external services after validation + * + * @param req - The HonoRequest object to clone + * @returns A Promise that resolves to a new Request object with the same properties + * @throws {HTTPException} If the request body was consumed directly via `req.raw` + * without using HonoRequest methods (e.g., `req.json()`, `req.text()`), making it + * impossible to reconstruct the body from cache + * + * @example + * ```ts + * // Clone after consuming the body (e.g., after validation) + * app.post('/forward', + * validator('json', (data) => data), + * async (c) => { + * const validated = c.req.valid('json') + * // Body has been consumed, but cloneRawRequest still works + * const clonedReq = await cloneRawRequest(c.req) + * return fetch('http://backend-service.com', clonedReq) + * } + * ) + * ``` + */ +export declare const cloneRawRequest: (req: HonoRequest) => Promise; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts new file mode 100644 index 000000000..0899a5d2b --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts @@ -0,0 +1,3 @@ +//#region src/request/constants.d.ts +export declare const GET_MATCH_RESULT: unique symbol; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts new file mode 100644 index 000000000..f00b40662 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts @@ -0,0 +1,98 @@ +//#region src/router.d.ts +/** + * @module + * This module provides types definitions and variables for the routers. + */ +/** + * Constant representing all HTTP methods in uppercase. + */ +export declare const METHOD_NAME_ALL: "ALL"; +/** + * Constant representing all HTTP methods in lowercase. + */ +export declare const METHOD_NAME_ALL_LOWERCASE: "all"; +/** + * Array of supported HTTP methods. + */ +export declare const METHODS: readonly ["get", "post", "put", "delete", "options", "patch", "query"]; +/** + * Error message indicating that a route cannot be added because the matcher is already built. + */ +export declare const MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built."; +/** + * Interface representing a router. + * + * @template T - The type of the handler. + */ +export interface Router { + /** + * The name of the router. + */ + name: string; + /** + * Adds a route to the router. + * + * @param method - The HTTP method (e.g., 'get', 'post'). + * @param path - The path for the route. + * @param handler - The handler for the route. + */ + add(method: string, path: string, handler: T): void; + /** + * Matches a route based on the given method and path. + * + * @param method - The HTTP method (e.g., 'get', 'post'). + * @param path - The path to match. + * @returns The result of the match. + */ + match(method: string, path: string): Result; +} +/** + * Type representing a map of parameter indices. + */ +export type ParamIndexMap = Record; +/** + * Type representing a stash of parameters. + */ +export type ParamStash = string[]; +/** + * Type representing a map of parameters. + */ +export type Params = Record; +/** + * Type representing the result of a route match. + * + * The result can be in one of two formats: + * 1. An array of handlers with their corresponding parameter index maps, followed by a parameter stash. + * 2. An array of handlers with their corresponding parameter maps. + * + * Example: + * + * [[handler, paramIndexMap][], paramArray] + * ```typescript + * [ + * [ + * [middlewareA, {}], // '*' + * [funcA, {'id': 0}], // '/user/:id/*' + * [funcB, {'id': 0, 'action': 1}], // '/user/:id/:action' + * ], + * ['123', 'abc'] + * ] + * ``` + * + * [[handler, params][]] + * ```typescript + * [ + * [ + * [middlewareA, {}], // '*' + * [funcA, {'id': '123'}], // '/user/:id/*' + * [funcB, {'id': '123', 'action': 'abc'}], // '/user/:id/:action' + * ] + * ] + * ``` + */ +export type Result = [[T, ParamIndexMap][], ParamStash] | [[T, Params][]]; +/** + * Error class representing an unsupported path error. + */ +export declare class UnsupportedPathError extends Error {} +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts new file mode 100644 index 000000000..4a0be1dea --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts @@ -0,0 +1,214 @@ +import { HonoBase as Hono } from "./hono-base.js"; +import { CustomHeader, RequestHeader } from "./utils/headers.js"; +import { StatusCode } from "./utils/http-status.js"; +import { IfAnyThenEmptyObject, IsAny, JSONValue, RemoveBlankRecord, Simplify, UnionToIntersection } from "./utils/types.js"; +import { Context } from "./context.js"; +//#region src/types.d.ts +export type Bindings = object; +export type Variables = object; +export type BlankEnv = {}; +export type Env = { + Bindings?: Bindings; + Variables?: Variables; +}; +export type Next = () => Promise; +export type ExtractInput = I extends Input ? unknown extends I['in'] ? {} : I['in'] : I; +export type Input = { + in?: {}; + out?: {}; + outputFormat?: ResponseFormat; +}; +export type BlankSchema = {}; +export type BlankInput = {}; +export interface RouterRoute { + basePath: string; + path: string; + method: string; + handler: H; +} +export type HandlerResponse = Response | TypedResponse | Promise> | Promise; +export type Handler = any> = (c: Context, next: Next) => R; +export type MiddlewareHandler = Response> = (c: Context, next: Next) => Promise; +export type H = any> = Handler | MiddlewareHandler; +/** + * You can extend this interface to define a custom `c.notFound()` Response type. + * + * @example + * declare module 'hono' { + * interface NotFoundResponse extends Response, TypedResponse {} + * } + */ +export interface NotFoundResponse {} +export type NotFoundHandler = (c: Context) => NotFoundResponse extends Response ? NotFoundResponse | Promise : Response | Promise; +export interface HTTPResponseError extends Error { + getResponse: () => Response; +} +export type ErrorHandler = (err: Error | HTTPResponseError, c: Context) => Response | Promise; +export interface HandlerInterface { +

= any, E2 extends Env = E>(handler: H): Hono, S & ToSchema>, BasePath, CurrentPath>; +

= any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(...handlers: [H & M1, H]): Hono, S & ToSchema | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(path: P, handler: H): Hono, S, M, P, I, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(path: P, ...handlers: [H & M1, H]): Hono | MergeMiddlewareResponse, S, M, P, I2, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I3, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I4, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I5, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I6, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I7, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I8, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I9, BasePath>, BasePath, MergePath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I10, BasePath>, BasePath, MergePath>; +

= any>(...handlers: H[]): Hono>, BasePath, CurrentPath>; +

= any>(path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; +

= any, I extends Input = BlankInput>(path: P): Hono, I, MergeTypedResponse>, BasePath, MergePath>; +} +export interface MiddlewareHandlerInterface { + (...handlers: MiddlewareHandler>[]): Hono, S, BasePath, MergePath>; + (handler: MiddlewareHandler>): Hono, S, BasePath, MergePath>; + , P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E>(path: P, handler: MiddlewareHandler): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; +

(path: P, ...handlers: MiddlewareHandler>[]): Hono>; +} +export interface OnHandlerInterface { + , R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(method: M, path: P, handler: H): Hono, S & ToSchema, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(method: M, path: P, ...handlers: [H & M1, H]): Hono, S & ToSchema, I2, MergeTypedResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema, I3, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema, I4, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema, I5, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema, I6, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema, I7, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema, I8, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema, I9, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema, I10, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + = any, I extends Input = BlankInput>(method: M, path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(methods: M[], path: P, handler: H): Hono, S & ToSchema, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H]): Hono, S & ToSchema, I2, MergeTypedResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema, I3, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema, I4, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema, I5, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema, I6, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema, I7, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema, I8, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema, I9, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema, I10, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + = any, I extends Input = BlankInput>(methods: M[], path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; + = any, E2 extends Env = E>(methods: M | M[], paths: Ps, ...handlers: H, I, R>[]): Hono, I, MergeTypedResponse>, BasePath, Ps extends [...string[], infer LastPath extends string] ? MergePath : never>; +} +type ToSchemaOutput = RorO extends TypedResponse ? { + output: unknown extends T ? {} : T; + outputFormat: I extends { + outputFormat: string; + } ? I['outputFormat'] : F; + status: U; +} : { + output: unknown extends RorO ? {} : RorO; + outputFormat: unknown extends RorO ? 'json' : I extends { + outputFormat: string; + } ? I['outputFormat'] : 'json'; + status: StatusCode; +}; +export type ToSchema = IsAny extends true ? { [K in P]: { [K2 in M as AddDollar]: { + input: AddParam, P>; + output: {}; + outputFormat: ResponseFormat; + status: StatusCode; +}; }; } : [RorO] extends [never] ? {} : [RorO] extends [Promise] ? {} : { [K in P]: { [K2 in M as AddDollar]: Simplify<{ + input: AddParam, P>; +} & ToSchemaOutput>; }; }; +export type Schema = { + [Path: string]: { + [Method: `$${Lowercase}`]: Endpoint; + }; +}; +type AddSchemaIfHasResponse = [Merged] extends [Promise] ? S : S & ToSchema, I, Merged>; +export type Endpoint = { + input: any; + output: any; + outputFormat: ResponseFormat; + status: StatusCode; +}; +type ExtractParams = string extends Path ? Record : Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string; } : Path extends `${infer _Start}:${infer Param}` ? { [K in Param]: string; } : never; +type FlattenIfIntersect = T extends (infer O) ? { [K in keyof O]: O[K]; } : never; +export type MergeSchemaPath = { [P in keyof OrigSchema as MergePath]: [OrigSchema[P]] extends [Record] ? { [M in keyof OrigSchema[P]]: MergeEndpointParamsWithPath; } : never; }; +type MergeEndpointParamsWithPath = T extends unknown ? { + input: T['input'] extends { + param: infer _; + } ? ExtractParams extends never ? T['input'] : FlattenIfIntersect as K extends `${infer Prefix}{${infer _}}` ? Prefix : K]: string; }; + }> : RemoveBlankRecord> extends never ? T['input'] : T['input'] & { + param: { [K in keyof ExtractParams as K extends `${infer Prefix}{${infer _}}` ? Prefix : K]: string; }; + }; + output: T['output']; + outputFormat: T['outputFormat']; + status: T['status']; +} : never; +export type AddParam = ParamKeys

extends never ? I : I extends { + param: infer _; +} ? I : I & { + param: UnionToIntersection>>; +}; +type AddDollar = `$${Lowercase}`; +export type MergePath = B extends '' ? MergePath : A extends '' ? B : A extends '/' ? B : A extends `${infer P}/` ? B extends `/${infer Q}` ? `${P}/${Q}` : `${P}/${B}` : B extends `/${infer Q}` ? Q extends '' ? A : `${A}/${Q}` : `${A}/${B}`; +export type KnownResponseFormat = 'json' | 'text' | 'redirect'; +export type ResponseFormat = KnownResponseFormat | string; +export type TypedResponse = { + _data: T; + _status: U; + _format: F; +}; +type MergeTypedResponse = T extends Promise ? T : T extends Promise ? T2 extends TypedResponse ? T2 : TypedResponse : T extends TypedResponse ? T : TypedResponse; +type ExtractTypedResponseOnly = T extends TypedResponse ? T : never; +type MergeMiddlewareResponse = T extends ((c: any, next: any) => Promise) ? Exclude extends never ? never : Exclude extends Response | TypedResponse ? ExtractTypedResponseOnly> : never : T extends ((c: any, next: any) => infer R) ? R extends Response | TypedResponse ? ExtractTypedResponseOnly : never : never; +export type FormValue = string | Blob; +export type ParsedFormValue = string | File; +export type ValidationTargets = { + json: any; + form: Record; + query: Record; + param: Record; + header: Record; + cookie: Record; +}; +type ParamKey = Component extends `:${infer NameWithPattern}` ? NameWithPattern extends `${infer Name}{${infer Rest}` ? Rest extends `${infer _Pattern}?` ? `${Name}?` : Name : NameWithPattern : never; +export type ParamKeys = Path extends `${infer Component}/${infer Rest}` ? ParamKey | ParamKeys : ParamKey; +export type ParamKeyToRecord = T extends `${infer R}?` ? Record : { [K in T]: string; }; +export type InputToDataByTarget = T extends { [K in Target]: infer R; } ? R : never; +export type RemoveQuestion = T extends `${infer R}?` ? R : T; +export type ExtractSchema = UnionToIntersection ? S : never>; +export type ExtractSchemaForStatusCode = { [Path in keyof ExtractSchema]: { [Method in keyof ExtractSchema[Path]]: Extract[Path][Method], { + status: Status; +}>; }; }; +export type ExtractHandlerResponse = T extends ((c: any, next: any) => Promise) ? Exclude extends never ? never : Exclude extends Response | TypedResponse ? Exclude : never : T extends ((c: any, next: any) => infer R) ? R extends Response | TypedResponse ? R : never : never; +type ProcessHead = IfAnyThenEmptyObject; +export type IntersectNonAnyTypes = T extends [infer Head, ...infer Rest] ? ProcessHead & IntersectNonAnyTypes : {}; +export declare abstract class FetchEventLike { + abstract readonly request: Request; + abstract respondWith(promise: Response | Promise): void; + abstract passThroughOnException(): void; + abstract waitUntil(promise: Promise): void; +} +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts new file mode 100644 index 000000000..9bbf8cafd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts @@ -0,0 +1,75 @@ +import { HonoRequest } from "../request.js"; +//#region src/utils/body.d.ts +type BodyDataValueDot = { + [x: string]: string | File | BodyDataValueDot; +}; +type BodyDataValueDotAll = { + [x: string]: string | File | (string | File)[] | BodyDataValueDotAll; +}; +type SimplifyBodyData = { [K in keyof T]: string | File | (string | File)[] | BodyDataValueDotAll extends T[K] ? string | File | (string | File)[] | BodyDataValueDotAll : string | File | BodyDataValueDot extends T[K] ? string | File | BodyDataValueDot : string | File | (string | File)[] extends T[K] ? string | File | (string | File)[] : string | File; } & {}; +type BodyDataValueComponent = string | File | (T extends { + all: false; +} ? never : T extends { + all: true; +} | { + all: boolean; +} ? (string | File)[] : never); +type BodyDataValueObject = { + [key: string]: BodyDataValueComponent | BodyDataValueObject; +}; +type BodyDataValue = BodyDataValueComponent | (T extends { + dot: false; +} ? never : T extends { + dot: true; +} | { + dot: boolean; +} ? BodyDataValueObject : never); +export type BodyData = {}> = SimplifyBodyData>>; +export type ParseBodyOptions = { + /** + * Determines whether all fields with multiple values should be parsed as arrays. + * @default false + * @example + * const data = new FormData() + * data.append('file', 'aaa') + * data.append('file', 'bbb') + * data.append('message', 'hello') + * + * If all is false: + * parseBody should return { file: 'bbb', message: 'hello' } + * + * If all is true: + * parseBody should return { file: ['aaa', 'bbb'], message: 'hello' } + */ + all: boolean; + /** + * Determines whether all fields with dot notation should be parsed as nested objects. + * @default false + * @example + * const data = new FormData() + * data.append('obj.key1', 'value1') + * data.append('obj.key2', 'value2') + * + * If dot is false: + * parseBody should return { 'obj.key1': 'value1', 'obj.key2': 'value2' } + * + * If dot is true: + * parseBody should return { obj: { key1: 'value1', key2: 'value2' } } + */ + dot: boolean; +}; +/** + * Parses the body of a request based on the provided options. + * + * @template T - The type of the parsed body data. + * @param {HonoRequest | Request} request - The request object to parse. + * @param {Partial} [options] - Options for parsing the body. + * @returns {Promise} The parsed body data. + */ +interface ParseBody { + , T extends BodyData>(request: HonoRequest | Request, options?: Options): Promise; + (request: HonoRequest | Request, options?: Partial): Promise; +} +export declare const parseBody: ParseBody; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts new file mode 100644 index 000000000..d3edf8c77 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts @@ -0,0 +1,10 @@ +//#region src/utils/headers.d.ts +/** + * @module + * HTTP Headers utility. + */ +export type RequestHeader = 'A-IM' | 'Accept' | 'Accept-Additions' | 'Accept-CH' | 'Accept-Charset' | 'Accept-Datetime' | 'Accept-Encoding' | 'Accept-Features' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Post' | 'Accept-Query' | 'Accept-Ranges' | 'Accept-Signature' | 'Access-Control' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Activate-Storage-Access' | 'Age' | 'Allow' | 'ALPN' | 'Alt-Svc' | 'Alt-Used' | 'Alternates' | 'AMP-Cache-Transform' | 'Apply-To-Redirect-Ref' | 'Authentication-Control' | 'Authentication-Info' | 'Authorization' | 'Available-Dictionary' | 'C-Ext' | 'C-Man' | 'C-Opt' | 'C-PEP' | 'C-PEP-Info' | 'Cache-Control' | 'Cache-Group-Invalidation' | 'Cache-Groups' | 'Cache-Status' | 'Cal-Managed-ID' | 'CalDAV-Timezones' | 'Capsule-Protocol' | 'CDN-Cache-Control' | 'CDN-Loop' | 'Cert-Not-After' | 'Cert-Not-Before' | 'Clear-Site-Data' | 'Client-Cert' | 'Client-Cert-Chain' | 'Close' | 'CMCD-Object' | 'CMCD-Request' | 'CMCD-Session' | 'CMCD-Status' | 'CMSD-Dynamic' | 'CMSD-Static' | 'Concealed-Auth-Export' | 'Configuration-Context' | 'Connection' | 'Content-Base' | 'Content-Digest' | 'Content-Disposition' | 'Content-Encoding' | 'Content-ID' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-MD5' | 'Content-Range' | 'Content-Script-Type' | 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only' | 'Content-Style-Type' | 'Content-Type' | 'Content-Version' | 'Cookie' | 'Cookie2' | 'Cross-Origin-Embedder-Policy' | 'Cross-Origin-Embedder-Policy-Report-Only' | 'Cross-Origin-Opener-Policy' | 'Cross-Origin-Opener-Policy-Report-Only' | 'Cross-Origin-Resource-Policy' | 'CTA-Common-Access-Token' | 'DASL' | 'Date' | 'DAV' | 'Default-Style' | 'Delta-Base' | 'Deprecation' | 'Depth' | 'Derived-From' | 'Destination' | 'Detached-JWS' | 'Differential-ID' | 'Dictionary-ID' | 'Digest' | 'DPoP' | 'DPoP-Nonce' | 'Early-Data' | 'EDIINT-Features' | 'ETag' | 'Expect' | 'Expect-CT' | 'Expires' | 'Ext' | 'Forwarded' | 'From' | 'GetProfile' | 'Hobareg' | 'Host' | 'HTTP2-Settings' | 'If' | 'If-Match' | 'If-Modified-Since' | 'If-None-Match' | 'If-Range' | 'If-Schedule-Tag-Match' | 'If-Unmodified-Since' | 'IM' | 'Include-Referred-Token-Binding-ID' | 'Incremental' | 'Isolation' | 'Keep-Alive' | 'Label' | 'Last-Event-ID' | 'Last-Modified' | 'Link' | 'Link-Template' | 'Location' | 'Lock-Token' | 'Man' | 'Max-Forwards' | 'Memento-Datetime' | 'Meter' | 'Method-Check' | 'Method-Check-Expires' | 'MIME-Version' | 'Negotiate' | 'NEL' | 'OData-EntityId' | 'OData-Isolation' | 'OData-MaxVersion' | 'OData-Version' | 'Opt' | 'Optional-WWW-Authenticate' | 'Ordering-Type' | 'Origin' | 'Origin-Agent-Cluster' | 'OSCORE' | 'OSLC-Core-Version' | 'Overwrite' | 'P3P' | 'PEP' | 'PEP-Info' | 'Permissions-Policy' | 'PICS-Label' | 'Ping-From' | 'Ping-To' | 'Position' | 'Pragma' | 'Prefer' | 'Preference-Applied' | 'Priority' | 'ProfileObject' | 'Protocol' | 'Protocol-Info' | 'Protocol-Query' | 'Protocol-Request' | 'Proxy-Authenticate' | 'Proxy-Authentication-Info' | 'Proxy-Authorization' | 'Proxy-Features' | 'Proxy-Instruction' | 'Proxy-Status' | 'Public' | 'Public-Key-Pins' | 'Public-Key-Pins-Report-Only' | 'Range' | 'Redirect-Ref' | 'Referer' | 'Referer-Root' | 'Referrer-Policy' | 'Refresh' | 'Repeatability-Client-ID' | 'Repeatability-First-Sent' | 'Repeatability-Request-ID' | 'Repeatability-Result' | 'Replay-Nonce' | 'Reporting-Endpoints' | 'Repr-Digest' | 'Retry-After' | 'Safe' | 'Schedule-Reply' | 'Schedule-Tag' | 'Sec-Fetch-Dest' | 'Sec-Fetch-Mode' | 'Sec-Fetch-Site' | 'Sec-Fetch-Storage-Access' | 'Sec-Fetch-User' | 'Sec-GPC' | 'Sec-Purpose' | 'Sec-Token-Binding' | 'Sec-WebSocket-Accept' | 'Sec-WebSocket-Extensions' | 'Sec-WebSocket-Key' | 'Sec-WebSocket-Protocol' | 'Sec-WebSocket-Version' | 'Security-Scheme' | 'Server' | 'Server-Timing' | 'Set-Cookie' | 'Set-Cookie2' | 'Set-Txn' | 'SetProfile' | 'Signature' | 'Signature-Input' | 'SLUG' | 'SoapAction' | 'Status-URI' | 'Strict-Transport-Security' | 'Sunset' | 'Surrogate-Capability' | 'Surrogate-Control' | 'TCN' | 'TE' | 'Timeout' | 'Timing-Allow-Origin' | 'Topic' | 'Traceparent' | 'Tracestate' | 'Trailer' | 'Transfer-Encoding' | 'TTL' | 'Unencoded-Digest' | 'Upgrade' | 'Urgency' | 'URI' | 'Use-As-Dictionary' | 'User-Agent' | 'Variant-Vary' | 'Vary' | 'Via' | 'Want-Content-Digest' | 'Want-Digest' | 'Want-Repr-Digest' | 'Want-Unencoded-Digest' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-Frame-Options'; +export type ResponseHeader = 'Accept-Query' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Activate-Storage-Access' | 'Age' | 'Allow' | 'Cache-Control' | 'Cache-Group-Invalidation' | 'Cache-Groups' | 'Clear-Site-Data' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-Range' | 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only' | 'Content-Type' | 'Cookie' | 'Cross-Origin-Embedder-Policy' | 'Cross-Origin-Opener-Policy' | 'Cross-Origin-Resource-Policy' | 'Date' | 'ETag' | 'Expires' | 'Incremental' | 'Last-Modified' | 'Location' | 'Permissions-Policy' | 'Pragma' | 'Retry-After' | 'Save-Data' | 'Sec-CH-Prefers-Color-Scheme' | 'Sec-CH-Prefers-Reduced-Motion' | 'Sec-CH-UA' | 'Sec-CH-UA-Arch' | 'Sec-CH-UA-Bitness' | 'Sec-CH-UA-Form-Factor' | 'Sec-CH-UA-Full-Version' | 'Sec-CH-UA-Full-Version-List' | 'Sec-CH-UA-Mobile' | 'Sec-CH-UA-Model' | 'Sec-CH-UA-Platform' | 'Sec-CH-UA-Platform-Version' | 'Sec-CH-UA-WoW64' | 'Sec-Fetch-Dest' | 'Sec-Fetch-Mode' | 'Sec-Fetch-Site' | 'Sec-Fetch-User' | 'Sec-GPC' | 'Server' | 'Server-Timing' | 'Service-Worker-Navigation-Preload' | 'Set-Cookie' | 'Set-Txn' | 'Strict-Transport-Security' | 'Timing-Allow-Origin' | 'Trailer' | 'Transfer-Encoding' | 'Unencoded-Digest' | 'Upgrade' | 'Vary' | 'Want-Unencoded-Digest' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-DNS-Prefetch-Control' | 'X-Frame-Options' | 'X-Permitted-Cross-Domain-Policies' | 'X-Powered-By' | 'X-Robots-Tag' | 'X-XSS-Protection'; +export type AcceptHeader = 'Accept' | 'Accept-Charset' | 'Accept-Encoding' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Post' | 'Accept-Ranges'; +export type CustomHeader = string & {}; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts new file mode 100644 index 000000000..4c48304e2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts @@ -0,0 +1,34 @@ +//#region src/utils/http-status.d.ts +/** + * @module + * HTTP Status utility. + */ +export type InfoStatusCode = 100 | 101 | 102 | 103; +export type SuccessStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226; +export type DeprecatedStatusCode = 305 | 306; +export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | DeprecatedStatusCode | 307 | 308; +export type ClientErrorStatusCode = 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451; +export type ServerErrorStatusCode = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511; +/** + * `UnofficialStatusCode` can be used to specify an unofficial status code. + * @example + * + * ```ts + * app.get('/unknown', (c) => { + * return c.text("Unknown Error", 520 as UnofficialStatusCode) + * }) + * ``` + */ +export type UnofficialStatusCode = -1; +/** + * @deprecated + * Use `UnofficialStatusCode` instead. + */ +export type UnOfficalStatusCode = UnofficialStatusCode; +/** + * If you want to use an unofficial status, use `UnofficialStatusCode`. + */ +export type StatusCode = InfoStatusCode | SuccessStatusCode | RedirectStatusCode | ClientErrorStatusCode | ServerErrorStatusCode | UnofficialStatusCode; +export type ContentlessStatusCode = 101 | 204 | 205 | 304; +export type ContentfulStatusCode = Exclude; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts new file mode 100644 index 000000000..b4ea3597e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts @@ -0,0 +1,73 @@ +//#region src/utils/mime.d.ts +/** + * @module + * MIME utility. + */ +export declare const getMimeType: (filename: string, mimes?: Record) => string | undefined; +export declare const getExtension: (mimeType: string) => string | undefined; +/** + * Union types for BaseMime + */ +export type BaseMime = (typeof _baseMimes)[keyof typeof _baseMimes]; +declare const _baseMimes: { + readonly aac: "audio/aac"; + readonly avi: "video/x-msvideo"; + readonly avif: "image/avif"; + readonly av1: "video/av1"; + readonly bin: "application/octet-stream"; + readonly bmp: "image/bmp"; + readonly css: "text/css; charset=utf-8"; + readonly csv: "text/csv; charset=utf-8"; + readonly eot: "application/vnd.ms-fontobject"; + readonly epub: "application/epub+zip"; + readonly gif: "image/gif"; + readonly gz: "application/gzip"; + readonly htm: "text/html; charset=utf-8"; + readonly html: "text/html; charset=utf-8"; + readonly ico: "image/x-icon"; + readonly ics: "text/calendar; charset=utf-8"; + readonly jpeg: "image/jpeg"; + readonly jpg: "image/jpeg"; + readonly js: "text/javascript; charset=utf-8"; + readonly json: "application/json"; + readonly jsonld: "application/ld+json"; + readonly map: "application/json"; + readonly mid: "audio/x-midi"; + readonly midi: "audio/x-midi"; + readonly mjs: "text/javascript; charset=utf-8"; + readonly mp3: "audio/mpeg"; + readonly mp4: "video/mp4"; + readonly mpeg: "video/mpeg"; + readonly oga: "audio/ogg"; + readonly ogv: "video/ogg"; + readonly ogx: "application/ogg"; + readonly opus: "audio/opus"; + readonly otf: "font/otf"; + readonly pdf: "application/pdf"; + readonly png: "image/png"; + readonly rtf: "application/rtf"; + readonly svg: "image/svg+xml; charset=utf-8"; + readonly tif: "image/tiff"; + readonly tiff: "image/tiff"; + readonly ts: "video/mp2t"; + readonly ttf: "font/ttf"; + readonly txt: "text/plain; charset=utf-8"; + readonly wasm: "application/wasm"; + readonly webm: "video/webm"; + readonly weba: "audio/webm"; + readonly webmanifest: "application/manifest+json"; + readonly webp: "image/webp"; + readonly woff: "font/woff"; + readonly woff2: "font/woff2"; + readonly xhtml: "application/xhtml+xml; charset=utf-8"; + readonly xml: "application/xml; charset=utf-8"; + readonly zip: "application/zip"; + readonly '3gp': "video/3gpp"; + readonly '3g2': "video/3gpp2"; + readonly gltf: "model/gltf+json"; + readonly glb: "model/gltf-binary"; +}; +declare const baseMimes: Record; +//#endregion +export { baseMimes as mimes }; +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts new file mode 100644 index 000000000..2a4ad3781 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts @@ -0,0 +1,62 @@ +//#region src/utils/types.d.ts +/** + * @module + * Types utility. + */ +export type Expect = T; +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; +export type NotEqual = true extends Equal ? false : true; +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +export type RemoveBlankRecord = T extends Record ? (K extends string ? T : never) : never; +export type IfAnyThenEmptyObject = 0 extends 1 & T ? {} : T; +export type JSONPrimitive = string | boolean | number | null; +export type JSONArray = (JSONPrimitive | JSONObject | JSONArray)[]; +export type JSONObject = { + [key: string]: JSONPrimitive | JSONArray | JSONObject | object | InvalidJSONValue; +}; +export type InvalidJSONValue = undefined | symbol | ((...args: unknown[]) => unknown); +type InvalidToNull = T extends InvalidJSONValue ? null : T; +type IsInvalid = T extends InvalidJSONValue ? true : false; +/** + * symbol keys are omitted through `JSON.stringify` + */ +type OmitSymbolKeys = { [K in keyof T as K extends symbol ? never : K]: T[K]; }; +export type JSONValue = JSONObject | JSONArray | JSONPrimitive; +/** + * Convert a type to a JSON-compatible type. + * + * Non-JSON values such as `Date` implement `.toJSON()`, + * so they can be transformed to a value assignable to `JSONObject` + * + * `JSON.stringify()` throws a `TypeError` when it encounters a `bigint` value, + * unless a custom `replacer` function or `.toJSON()` method is provided. + * + * This behavior can be controlled by the `TError` generic type parameter, + * which defaults to `bigint | ReadonlyArray`. + * You can set it to `never` to disable this check. + */ +export type JSONParsed> = T extends { + toJSON(): infer J; +} ? (() => J) extends (() => JSONPrimitive) ? J : (() => J) extends (() => { + toJSON(): unknown; +}) ? {} : JSONParsed : T extends JSONPrimitive ? T : T extends InvalidJSONValue ? never : T extends ReadonlyArray ? { [K in keyof T]: JSONParsed, TError>; } extends (infer A) ? A extends ReadonlyArray ? A : JSONParsed, TError>[] : never : T extends Set | Map | Record ? {} : T extends object ? T[keyof T] extends TError ? never : { [K in keyof OmitSymbolKeys as IsInvalid extends true ? never : K]: boolean extends IsInvalid ? JSONParsed | undefined : JSONParsed; } : T extends unknown ? T extends TError ? never : JSONValue : never; +/** + * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. + * @copyright from sindresorhus/type-fest + */ +export type Simplify = { [KeyType in keyof T]: T[KeyType]; } & {}; +/** + * A simple extension of Simplify that will deeply traverse array elements. + */ +export type SimplifyDeepArray = T extends any[] ? { [E in keyof T]: SimplifyDeepArray; } : Simplify; +export type InterfaceToType = T extends Function ? T : { [K in keyof T]: InterfaceToType; }; +export type RequiredKeysOf = Exclude<{ [Key in keyof BaseType]: BaseType extends Record ? Key : never; }[keyof BaseType], undefined>; +export type HasRequiredKeys = RequiredKeysOf extends never ? false : true; +export type IsAny = boolean extends (T extends never ? true : false) ? true : false; +/** + * String literal types with auto-completion + * @see https://github.com/Microsoft/TypeScript/issues/29729 + */ +export type StringLiteralUnion = T | (string & Record); +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts new file mode 100644 index 000000000..3a494895e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts @@ -0,0 +1,3 @@ +import { InferInput } from "./utils.js"; +import { ValidationFunction, validator } from "./validator.js"; +export { type InferInput, type ValidationFunction, validator }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts new file mode 100644 index 000000000..13159be3c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts @@ -0,0 +1,27 @@ +import { UnionToIntersection } from "../utils/types.js"; +import { FormValue, ParsedFormValue, ValidationTargets } from "../types.js"; +//#region src/validator/utils.d.ts +/** + * Checks if T is a literal union type (e.g., 'asc' | 'desc') + * that should be preserved in input types. + * Returns true for union literals, false for single literals or wide types. + */ +export type IsLiteralUnion = [Exclude] extends [Base] ? [Exclude] extends [UnionToIntersection>] ? false : true : false; +type IsOptionalUnion = [unknown] extends [T] ? false : undefined extends T ? true : false; +type SimplifyDeep = { [K in keyof T]: T[K]; } & {}; +type InferInputInner = SimplifyDeep<{ [K in keyof Output]: IsLiteralUnion extends true ? Output[K] : IsOptionalUnion extends true ? Output[K] : Target extends 'form' ? T | T[] : Target extends 'query' ? string | string[] : Target extends 'param' ? string : Target extends 'header' ? string : Target extends 'cookie' ? string : unknown; }>; +/** + * Utility type to infer input types for validation targets. + * Preserves literal union types (e.g., 'asc' | 'desc') while using + * the default ValidationTargets type for other values. + * + * @example + * ```ts + * // In @hono/zod-validator or similar: + * type Input = InferInput, 'query'> + * // { orderBy: 'asc' | 'desc', page: string | string[] } + * ``` + */ +export type InferInput = [Exclude] extends [never] ? {} : [Exclude] extends [object] ? undefined extends Output ? SimplifyDeep, Target, T>> | undefined : SimplifyDeep> : {}; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts new file mode 100644 index 000000000..85e09a1a3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts @@ -0,0 +1,18 @@ +import { Env, FormValue, MiddlewareHandler, TypedResponse, ValidationTargets } from "../types.js"; +import { Context } from "../context.js"; +import { InferInput } from "./utils.js"; +//#region src/validator/validator.d.ts +type ValidationTargetKeysWithBody = 'form' | 'json'; +type ValidationTargetByMethod = M extends 'get' | 'head' ? Exclude : keyof ValidationTargets; +export type ValidationFunction = (value: InputType, c: Context) => OutputType | TypedResponse | Promise | Promise; +export type ExtractValidationResponse = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends TypedResponse ? TypedResponse : PR extends Response ? PR : PR extends undefined ? never : never : R extends TypedResponse ? TypedResponse : R extends Response ? R : R extends undefined ? never : never : never; +export declare const validator: , P2 extends string = P, VF extends (value: unknown extends InputType ? ValidationTargets[U] : InputType, c: Context) => any = (value: unknown extends InputType ? ValidationTargets[U] : InputType, c: Context) => any, V extends { + in: { [K in U]: K extends "json" ? unknown extends InputType ? ExtractValidatorOutput : InputType : InferInput, K, FormValue>; }; + out: { [K in U]: ExtractValidatorOutput; }; +} = { + in: { [K in U]: K extends "json" ? unknown extends InputType ? ExtractValidatorOutput : InputType : InferInput, K, FormValue>; }; + out: { [K in U]: ExtractValidatorOutput; }; +}, E extends Env = any>(target: U, validationFunc: VF) => MiddlewareHandler>; +export type ExtractValidatorOutput = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends Response | TypedResponse ? never : PR : R extends Response | TypedResponse ? never : R : never; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/body.js b/tests/fixtures/npm/node_modules/hono/dist/utils/body.js new file mode 100644 index 000000000..6fe8e3031 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/body.js @@ -0,0 +1,96 @@ +import { bufferToFormData } from "./buffer.js"; +//#region src/utils/body.ts +const MAX_NESTED_OBJECTS = 1e4; +const isRawRequest = (request) => "headers" in request; +const parseBody = async (request, options = Object.create(null)) => { + const { all = false, dot = false } = options; + const mediaType = (isRawRequest(request) ? request.headers : request.raw.headers).get("Content-Type")?.split(";")[0].trim().toLowerCase(); + if (mediaType === "multipart/form-data" || mediaType === "application/x-www-form-urlencoded") return parseFormData(request, { + all, + dot + }); + return {}; +}; +/** +* Parses form data from a request. +* +* @template T - The type of the parsed body data. +* @param {HonoRequest | Request} request - The request object containing form data. +* @param {ParseBodyOptions} options - Options for parsing the form data. +* @returns {Promise} The parsed body data. +*/ +async function parseFormData(request, options) { + if (!isRawRequest(request) && request.bodyCache.formData) return convertFormDataToBodyData(await request.bodyCache.formData, options); + const headers = isRawRequest(request) ? request.headers : request.raw.headers; + const arrayBuffer = await request.arrayBuffer(); + const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || ""); + if (!isRawRequest(request)) request.bodyCache.formData = formDataPromise; + const formData = await formDataPromise; + if (formData) return convertFormDataToBodyData(formData, options); + return {}; +} +/** +* Converts form data to body data based on the provided options. +* +* @template T - The type of the parsed body data. +* @param {FormData} formData - The form data to convert. +* @param {ParseBodyOptions} options - Options for parsing the form data. +* @returns {T} The converted body data. +*/ +function convertFormDataToBodyData(formData, options) { + const form = Object.create(null); + const nestingState = { count: 0 }; + formData.forEach((value, key) => { + if (!(options.all || key.endsWith("[]"))) form[key] = value; + else handleParsingAllValues(form, key, value); + }); + if (options.dot) Object.entries(form).forEach(([key, value]) => { + if (key.includes(".")) { + handleParsingNestedValues(form, key, value, nestingState); + delete form[key]; + } + }); + return form; +} +/** +* Handles parsing all values for a given key, supporting multiple values as arrays. +* +* @param {BodyData} form - The form data object. +* @param {string} key - The key to parse. +* @param {FormDataEntryValue} value - The value to assign. +*/ +const handleParsingAllValues = (form, key, value) => { + if (form[key] !== void 0) { + if (Array.isArray(form[key])) form[key].push(value); + else form[key] = [form[key], value]; + } else if (!key.endsWith("[]")) form[key] = value; + else form[key] = [value]; +}; +/** +* Handles parsing nested values using dot notation keys. +* +* @param {BodyData} form - The form data object. +* @param {string} key - The dot notation key. +* @param {BodyDataValue} value - The value to assign. +*/ +const handleParsingNestedValues = (form, key, value, state) => { + if (/(?:^|\.)__proto__\./.test(key)) return; + let nestedForm = form; + const keys = key.split(".", 34); + if (keys.length > 33) throwNestingLimitExceeded(); + keys.forEach((key, index) => { + if (index === keys.length - 1) nestedForm[key] = value; + else { + if (!nestedForm[key] || typeof nestedForm[key] !== "object" || Array.isArray(nestedForm[key]) || nestedForm[key] instanceof File) { + if (state.count++ >= MAX_NESTED_OBJECTS) throwNestingLimitExceeded(); + nestedForm[key] = Object.create(null); + } + nestedForm = nestedForm[key]; + } + }); +}; +const throwNestingLimitExceeded = () => { + throw new Error("Nesting limit exceeded"); +}; +//#endregion +export { parseBody }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js b/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js new file mode 100644 index 000000000..8c6506452 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js @@ -0,0 +1,51 @@ +import { sha256 } from "./crypto.js"; +//#region src/utils/buffer.ts +/** +* @module +* Buffer utility. +*/ +const equal = (a, b) => { + if (a === b) return true; + if (a.byteLength !== b.byteLength) return false; + const va = new DataView(a); + const vb = new DataView(b); + let i = va.byteLength; + while (i--) if (va.getUint8(i) !== vb.getUint8(i)) return false; + return true; +}; +const constantTimeEqualString = (a, b) => { + const aLen = a.length; + const bLen = b.length; + const maxLen = Math.max(aLen, bLen); + let out = aLen ^ bLen; + for (let i = 0; i < maxLen; i++) { + const aChar = i < aLen ? a.charCodeAt(i) : 0; + const bChar = i < bLen ? b.charCodeAt(i) : 0; + out |= aChar ^ bChar; + } + return out === 0; +}; +const timingSafeEqualString = async (a, b, hashFunction) => { + if (!hashFunction) hashFunction = sha256; + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]); + if (sa == null || sb == null || typeof sa !== "string" || typeof sb !== "string") return false; + const hashEqual = constantTimeEqualString(sa, sb); + const originalEqual = constantTimeEqualString(a, b); + return hashEqual && originalEqual; +}; +const timingSafeEqual = async (a, b, hashFunction) => { + if (typeof a === "string" && typeof b === "string") return timingSafeEqualString(a, b, hashFunction); + if (!hashFunction) hashFunction = sha256; + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]); + if (!sa || !sb || typeof sa !== "string" || typeof sb !== "string") return false; + return timingSafeEqualString(sa, sb); +}; +const bufferToString = (buffer) => { + if (buffer instanceof ArrayBuffer) return new TextDecoder("utf-8").decode(buffer); + return buffer; +}; +const bufferToFormData = (arrayBuffer, contentType) => { + return new Response(arrayBuffer, { headers: { "Content-Type": contentType.replace(/^[^;]+/, (mediaType) => mediaType.toLowerCase()) } }).formData(); +}; +//#endregion +export { bufferToFormData, bufferToString, equal, timingSafeEqual }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js b/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js new file mode 100644 index 000000000..8b0615b69 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js @@ -0,0 +1,7 @@ +//#region src/utils/constants.ts +/** +* Constant used to mark a composed handler. +*/ +const COMPOSED_HANDLER = "__COMPOSED_HANDLER"; +//#endregion +export { COMPOSED_HANDLER }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js b/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js new file mode 100644 index 000000000..fd541131c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js @@ -0,0 +1,125 @@ +import { tryDecodeURIComponent } from "./url.js"; +//#region src/utils/cookie.ts +/** +* @module +* Cookie utility. +*/ +const algorithm = { + name: "HMAC", + hash: "SHA-256" +}; +const getCryptoKey = async (secret) => { + const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret; + return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]); +}; +const makeSignature = async (value, secret) => { + const key = await getCryptoKey(secret); + const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value)); + return btoa(String.fromCharCode(...new Uint8Array(signature))); +}; +const verifySignature = async (base64Signature, value, secret) => { + try { + const signatureBinStr = atob(base64Signature); + const signature = new Uint8Array(signatureBinStr.length); + for (let i = 0, len = signatureBinStr.length; i < len; i++) signature[i] = signatureBinStr.charCodeAt(i); + return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value)); + } catch { + return false; + } +}; +const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/; +const relaxedCookieNameRegEx = /^[!#-:<>-[\]-~]+$/; +const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/; +const trimCookieWhitespace = (value) => { + let start = 0; + let end = value.length; + while (start < end) { + const charCode = value.charCodeAt(start); + if (charCode !== 32 && charCode !== 9) break; + start++; + } + while (end > start) { + const charCode = value.charCodeAt(end - 1); + if (charCode !== 32 && charCode !== 9) break; + end--; + } + return start === 0 && end === value.length ? value : value.slice(start, end); +}; +const parse = (cookie, name) => { + if (name && cookie.indexOf(name) === -1) return {}; + const pairs = cookie.split(";"); + const parsedCookie = Object.create(null); + for (const pairStr of pairs) { + const valueStartPos = pairStr.indexOf("="); + if (valueStartPos === -1) continue; + const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos)); + if (name && name !== cookieName || !relaxedCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) continue; + let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1)); + if (cookieValue.startsWith("\"") && cookieValue.endsWith("\"")) cookieValue = cookieValue.slice(1, -1); + if (validCookieValueRegEx.test(cookieValue)) { + parsedCookie[cookieName] = tryDecodeURIComponent(cookieValue); + if (name) break; + } + } + return parsedCookie; +}; +const parseSigned = async (cookie, secret, name) => { + const parsedCookie = Object.create(null); + const secretKey = await getCryptoKey(secret); + for (const [key, value] of Object.entries(parse(cookie, name))) { + const signatureStartPos = value.lastIndexOf("."); + if (signatureStartPos < 0) continue; + const signedValue = value.substring(0, signatureStartPos); + const signature = value.substring(signatureStartPos + 1); + if (signature.length !== 44 || !signature.endsWith("=")) continue; + parsedCookie[key] = await verifySignature(signature, signedValue, secretKey) ? signedValue : false; + } + return parsedCookie; +}; +const _serialize = (name, value, opt = {}) => { + if (!validCookieNameRegEx.test(name)) throw new Error("Invalid cookie name"); + let cookie = `${name}=${value}`; + if (name.startsWith("__Secure-") && !opt.secure) throw new Error("__Secure- Cookie must have Secure attributes"); + if (name.startsWith("__Host-")) { + if (!opt.secure) throw new Error("__Host- Cookie must have Secure attributes"); + if (opt.path !== "/") throw new Error("__Host- Cookie must have Path attributes with \"/\""); + if (opt.domain) throw new Error("__Host- Cookie must not have Domain attributes"); + } + for (const key of [ + "domain", + "path", + "sameSite", + "priority" + ]) if (opt[key] && /[;\r\n]/.test(opt[key])) throw new Error(`${key} must not contain ";", "\\r", or "\\n"`); + if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) { + if (opt.maxAge > 3456e4) throw new Error("Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."); + cookie += `; Max-Age=${opt.maxAge | 0}`; + } + if (opt.domain && opt.prefix !== "host") cookie += `; Domain=${opt.domain}`; + if (opt.path) cookie += `; Path=${opt.path}`; + if (opt.expires) { + if (opt.expires.getTime() - Date.now() > 3456e7) throw new Error("Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."); + cookie += `; Expires=${opt.expires.toUTCString()}`; + } + if (opt.httpOnly) cookie += "; HttpOnly"; + if (opt.secure) cookie += "; Secure"; + if (opt.sameSite) cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`; + if (opt.priority) cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`; + if (opt.partitioned) { + if (!opt.secure) throw new Error("Partitioned Cookie must have Secure attributes"); + cookie += "; Partitioned"; + } + return cookie; +}; +const serialize = (name, value, opt) => { + value = encodeURIComponent(value); + return _serialize(name, value, opt); +}; +const serializeSigned = async (name, value, secret, opt = {}) => { + const signature = await makeSignature(value, secret); + value = `${value}.${signature}`; + value = encodeURIComponent(value); + return _serialize(name, value, opt); +}; +//#endregion +export { parse, parseSigned, serialize, serializeSigned }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js b/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js new file mode 100644 index 000000000..548867c49 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js @@ -0,0 +1,34 @@ +//#region src/utils/crypto.ts +const sha256 = async (data) => { + return await createHash(data, { + name: "SHA-256", + alias: "sha256" + }); +}; +const sha1 = async (data) => { + return await createHash(data, { + name: "SHA-1", + alias: "sha1" + }); +}; +const md5 = async (data) => { + return await createHash(data, { + name: "MD5", + alias: "md5" + }); +}; +const createHash = async (data, algorithm) => { + let sourceBuffer; + if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) sourceBuffer = data; + else { + if (typeof data === "object") data = JSON.stringify(data); + sourceBuffer = new TextEncoder().encode(String(data)); + } + if (crypto && crypto.subtle) { + const buffer = await crypto.subtle.digest({ name: algorithm.name }, sourceBuffer); + return Array.prototype.map.call(new Uint8Array(buffer), (x) => ("00" + x.toString(16)).slice(-2)).join(""); + } + return null; +}; +//#endregion +export { createHash, md5, sha1, sha256 }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/html.js b/tests/fixtures/npm/node_modules/hono/dist/utils/html.js new file mode 100644 index 000000000..7615ebdbd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/html.js @@ -0,0 +1,103 @@ +//#region src/utils/html.ts +/** +* @module +* HTML utility. +*/ +const HtmlEscapedCallbackPhase = { + Stringify: 1, + BeforeStream: 2, + Stream: 3 +}; +const raw = (value, callbacks) => { + const escapedString = new String(value); + escapedString.isEscaped = true; + escapedString.callbacks = callbacks; + return escapedString; +}; +const escapeRe = /[&<>'"]/; +const stringBufferToString = async (buffer, callbacks) => { + let str = ""; + callbacks ||= []; + const resolvedBuffer = await Promise.all(buffer); + for (let i = resolvedBuffer.length - 1;; i--) { + str += resolvedBuffer[i]; + i--; + if (i < 0) break; + let r = resolvedBuffer[i]; + if (typeof r === "object") callbacks.push(...r.callbacks || []); + const isEscaped = r.isEscaped; + r = await (typeof r === "object" ? r.toString() : r); + if (typeof r === "object") callbacks.push(...r.callbacks || []); + if (r.isEscaped ?? isEscaped) str += r; + else { + const buf = [str]; + escapeToBuffer(r, buf); + str = buf[0]; + } + } + return raw(str, callbacks); +}; +const escapeToBuffer = (str, buffer) => { + const match = str.search(escapeRe); + if (match === -1) { + buffer[0] += str; + return; + } + let escape; + let index; + let lastIndex = 0; + for (index = match; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: + escape = """; + break; + case 39: + escape = "'"; + break; + case 38: + escape = "&"; + break; + case 60: + escape = "<"; + break; + case 62: + escape = ">"; + break; + default: continue; + } + buffer[0] += str.substring(lastIndex, index) + escape; + lastIndex = index + 1; + } + buffer[0] += str.substring(lastIndex, index); +}; +const resolveCallbackSync = (str) => { + const callbacks = str.callbacks; + if (!callbacks?.length) return str; + const buffer = [str]; + const context = {}; + callbacks.forEach((c) => c({ + phase: HtmlEscapedCallbackPhase.Stringify, + buffer, + context + })); + return buffer[0]; +}; +const resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => { + if (typeof str === "object" && !(str instanceof String)) { + if (!(str instanceof Promise)) str = str.toString(); + if (str instanceof Promise) str = await str; + } + const callbacks = str.callbacks; + if (!callbacks?.length) return Promise.resolve(str); + if (buffer) buffer[0] += str; + else buffer = [str]; + const resStr = Promise.all(callbacks.map((c) => c({ + phase, + buffer, + context + }))).then((res) => Promise.all(res.filter(Boolean).map((str) => resolveCallback(str, phase, false, context, buffer))).then(() => buffer[0])); + if (preserveCallbacks) return raw(await resStr, callbacks); + else return resStr; +}; +//#endregion +export { HtmlEscapedCallbackPhase, escapeToBuffer, raw, resolveCallback, resolveCallbackSync, stringBufferToString }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/url.js b/tests/fixtures/npm/node_modules/hono/dist/utils/url.js new file mode 100644 index 000000000..8433d4cb9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/url.js @@ -0,0 +1,201 @@ +//#region src/utils/url.ts +const splitPath = (path) => { + const paths = path.split("/"); + if (paths[0] === "") paths.shift(); + return paths; +}; +const splitRoutingPath = (routePath) => { + const { groups, path } = extractGroupsFromPath(routePath); + const paths = splitPath(path); + return replaceGroupMarks(paths, groups); +}; +const extractGroupsFromPath = (path) => { + const groups = []; + path = path.replace(/\{[^}]+\}/g, (match, index) => { + const mark = `@${index}`; + groups.push([mark, match]); + return mark; + }); + return { + groups, + path + }; +}; +const replaceGroupMarks = (paths, groups) => { + for (let i = groups.length - 1; i >= 0; i--) { + const [mark] = groups[i]; + for (let j = paths.length - 1; j >= 0; j--) if (paths[j].includes(mark)) { + paths[j] = paths[j].replace(mark, groups[i][1]); + break; + } + } + return paths; +}; +const patternCache = {}; +const getPattern = (label, next) => { + if (label === "*") return "*"; + const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/); + if (match) { + const cacheKey = `${label}#${next}`; + if (!patternCache[cacheKey]) { + if (match[2]) patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [ + cacheKey, + match[1], + new RegExp(`^${match[2]}(?=/${next})`) + ] : [ + label, + match[1], + new RegExp(`^${match[2]}$`) + ]; + else patternCache[cacheKey] = [ + label, + match[1], + true + ]; + } + return patternCache[cacheKey]; + } + return null; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +/** +* Try to apply decodeURI() to given string. +* If it fails, skip invalid percent encoding or invalid UTF-8 sequences, and apply decodeURI() to the rest as much as possible. +* @param str The string to decode. +* @returns The decoded string that sometimes contains undecodable percent encoding. +* @example +* tryDecodeURI('Hello%20World') // 'Hello World' +* tryDecodeURI('Hello%20World/%A4%A2') // 'Hello World/%A4%A2' +*/ +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const getPath = (request) => { + const url = request.url; + const start = url.indexOf("/", url.indexOf(":") + 4); + let i = start; + for (; i < url.length; i++) { + const charCode = url.charCodeAt(i); + if (charCode === 37) { + const queryIndex = url.indexOf("?", i); + const hashIndex = url.indexOf("#", i); + const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex); + const path = url.slice(start, end); + return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path); + } else if (charCode === 63 || charCode === 35) break; + } + return url.slice(start, i); +}; +/** +* @deprecated +* Use the `URL` API instead. +*/ +const getQueryStrings = (url) => { + const queryIndex = url.indexOf("?", 8); + if (queryIndex === -1) return ""; + const hashIndex = url.indexOf("#", 8); + return hashIndex === -1 ? url.slice(queryIndex) : queryIndex < hashIndex ? url.slice(queryIndex, hashIndex) : ""; +}; +const getPathNoStrict = (request) => { + const result = getPath(request); + return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result; +}; +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +* @example +* mergePath('/api', '/users') // '/api/users' +* mergePath('/api/', '/users') // '/api/users' +* mergePath('/api', '/') // '/api' +* mergePath('/api/', '/') // '/api/' +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +const checkOptionalParameter = (path) => { + if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) return null; + const segments = path.split("/"); + const results = []; + let basePath = ""; + segments.forEach((segment) => { + if (segment !== "" && !/\:/.test(segment)) basePath += "/" + segment; + else if (/\:/.test(segment)) { + if (segment.charCodeAt(segment.length - 1) === 63) { + if (results.length === 0 && basePath === "") results.push("/"); + else results.push(basePath); + const optionalSegment = segment.slice(0, -1); + basePath += "/" + optionalSegment; + results.push(basePath); + } else basePath += "/" + segment; + } + }); + return results.filter((v, i, a) => a.indexOf(v) === i); +}; +const tryDecodeURIComponent = (str) => str.indexOf("%") !== -1 ? tryDecode(str, decodeURIComponent_) : str; +const _decodeURI = (value) => { + if (value.indexOf("+") !== -1) value = value.replace(/\+/g, " "); + return tryDecodeURIComponent(value); +}; +const _getQueryParam = (url, key, multiple) => { + const hashIndex = url.indexOf("#", 8); + if (hashIndex !== -1) url = url.slice(0, hashIndex); + let encoded; + if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) { + let keyIndex = url.indexOf("?", 8); + if (keyIndex === -1) return; + if (!url.startsWith(key, keyIndex + 1)) keyIndex = url.indexOf(`&${key}`, keyIndex + 1); + while (keyIndex !== -1) { + const trailingKeyCode = url.charCodeAt(keyIndex + key.length + 1); + if (trailingKeyCode === 61) { + const valueIndex = keyIndex + key.length + 2; + const endIndex = url.indexOf("&", valueIndex); + return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex)); + } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) return ""; + keyIndex = url.indexOf(`&${key}`, keyIndex + 1); + } + encoded = /[%+]/.test(url); + if (!encoded) return; + } + const results = Object.create(null); + encoded ??= /[%+]/.test(url); + let keyIndex = url.indexOf("?", 8); + while (keyIndex !== -1) { + const nextKeyIndex = url.indexOf("&", keyIndex + 1); + let valueIndex = url.indexOf("=", keyIndex); + if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) valueIndex = -1; + let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex); + if (encoded) name = _decodeURI(name); + keyIndex = nextKeyIndex; + if (name === "") continue; + let value; + if (valueIndex === -1) value = ""; + else { + value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex); + if (encoded) value = _decodeURI(value); + } + if (multiple) { + if (!(results[name] && Array.isArray(results[name]))) results[name] = []; + results[name].push(value); + } else results[name] ??= value; + } + return key ? results[key] : results; +}; +const getQueryParam = _getQueryParam; +const getQueryParams = (url, key) => { + return _getQueryParam(url, key, true); +}; +const decodeURIComponent_ = decodeURIComponent; +//#endregion +export { checkOptionalParameter, decodeURIComponent_, getPath, getPathNoStrict, getPattern, getQueryParam, getQueryParams, getQueryStrings, mergePath, splitPath, splitRoutingPath, tryDecode, tryDecodeURI, tryDecodeURIComponent }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/validator/index.js b/tests/fixtures/npm/node_modules/hono/dist/validator/index.js new file mode 100644 index 000000000..6e744aca6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/validator/index.js @@ -0,0 +1,2 @@ +import { validator } from "./validator.js"; +export { validator }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js b/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js new file mode 100644 index 000000000..1cc282071 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js @@ -0,0 +1,64 @@ +import { HTTPException } from "../http-exception.js"; +import { bufferToFormData } from "../utils/buffer.js"; +import { getCookie } from "../helper/cookie/index.js"; +//#region src/validator/validator.ts +const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i; +const multipartRegex = /^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/i; +const urlencodedRegex = /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i; +const validator = (target, validationFunc) => { + return async (c, next) => { + let value = {}; + const contentType = c.req.header("Content-Type"); + switch (target) { + case "json": + if (!contentType || !jsonRegex.test(contentType)) break; + try { + value = await c.req.json(); + } catch { + throw new HTTPException(400, { message: "Malformed JSON in request body" }); + } + break; + case "form": { + if (!contentType || !(multipartRegex.test(contentType) || urlencodedRegex.test(contentType))) break; + let formData; + if (c.req.bodyCache.formData) formData = await c.req.bodyCache.formData; + else try { + const arrayBuffer = await c.req.arrayBuffer(); + formData = await bufferToFormData(arrayBuffer, contentType); + c.req.bodyCache.formData = formData; + } catch (e) { + let message = "Malformed FormData request."; + message += e instanceof Error ? ` ${e.message}` : ` ${String(e)}`; + throw new HTTPException(400, { message }); + } + const form = Object.create(null); + formData.forEach((value, key) => { + if (key.endsWith("[]")) (form[key] ??= []).push(value); + else if (Array.isArray(form[key])) form[key].push(value); + else if (Object.hasOwn(form, key)) form[key] = [form[key], value]; + else form[key] = value; + }); + value = form; + break; + } + case "query": + value = Object.fromEntries(Object.entries(c.req.queries()).map(([k, v]) => { + return v.length === 1 ? [k, v[0]] : [k, v]; + })); + break; + case "param": + value = c.req.param(); + break; + case "header": + value = c.req.header(); + break; + case "cookie": value = getCookie(c); + } + const res = await validationFunc(value, c); + if (res instanceof Response) return res; + c.req.addValidatedData(target, res); + return await next(); + }; +}; +//#endregion +export { validator }; diff --git a/tests/fixtures/npm/node_modules/hono/package.json b/tests/fixtures/npm/node_modules/hono/package.json index 6264c824a..6c7dec6e9 100644 --- a/tests/fixtures/npm/node_modules/hono/package.json +++ b/tests/fixtures/npm/node_modules/hono/package.json @@ -1,10 +1,18 @@ { "name": "hono", "version": "4.13.13", - "description": "Test fixture: only the hono/ws helper that @hono/node-server's index imports.", + "description": "Test fixture: the hono core and validator (with their declarations) and the hono/ws helper that @hono/node-server's index imports.", "type": "module", "license": "MIT", "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js" + }, + "./validator": { + "types": "./dist/types/validator/index.d.ts", + "import": "./dist/validator/index.js" + }, "./ws": { "import": "./dist/helper/websocket/index.js" } diff --git a/tests/fixtures/npm/node_modules/zod/LICENSE b/tests/fixtures/npm/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/npm/node_modules/zod/index.d.cts b/tests/fixtures/npm/node_modules/zod/index.d.cts new file mode 100644 index 000000000..b8f3021a8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/index.d.cts @@ -0,0 +1,3 @@ +import * as z from "./v4/classic/external.cjs"; +export * from "./v4/classic/external.cjs"; +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/index.js b/tests/fixtures/npm/node_modules/zod/index.js new file mode 100644 index 000000000..4102a4715 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/index.js @@ -0,0 +1,7 @@ +import * as z from "./v4/classic/external.js"; +export * from "./v4/classic/external.js"; +// Aliasing the binding, instead of `export default z`, keeps the default export a +// re-export of `z` rather than a fresh namespace value. Rollup and Webpack can then +// shake `import z from "zod"` as they already shake `import { z } from "zod"` — without +// this they pull in every locale. See #6050. +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/package.json b/tests/fixtures/npm/node_modules/zod/package.json new file mode 100644 index 000000000..58b9916bd --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/package.json @@ -0,0 +1,23 @@ +{ + "name": "zod", + "version": "4.6.7", + "description": "Test fixture: zod 4's classic API, its core and locales, and the declarations @hono/zod-validator reads (the files the npm cases load).", + "type": "module", + "license": "MIT", + "types": "./index.d.cts", + "exports": { + ".": { + "types": "./index.d.cts", + "import": "./index.js" + }, + "./v3": { + "types": "./v3/index.d.cts" + }, + "./v4": { + "types": "./v4/index.d.cts" + }, + "./v4/core": { + "types": "./v4/core/index.d.cts" + } + } +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts b/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts new file mode 100644 index 000000000..384b36487 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.cjs"; +import { util, type ZodParsedType } from "./helpers/util.cjs"; +import type { TypeOf, ZodType } from "./index.cjs"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + too_big: "too_big"; + too_small: "too_small"; + not_multiple_of: "not_multiple_of"; + unrecognized_keys: "unrecognized_keys"; + invalid_union: "invalid_union"; + custom: "custom"; + invalid_literal: "invalid_literal"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + invalid_intersection_types: "invalid_intersection_types"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts b/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts new file mode 100644 index 000000000..205f3a096 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.cjs"; +import defaultErrorMap from "./locales/en.cjs"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/node_modules/zod/v3/external.d.cts b/tests/fixtures/npm/node_modules/zod/v3/external.d.cts new file mode 100644 index 000000000..ced9c1a85 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/external.d.cts @@ -0,0 +1,6 @@ +export * from "./errors.cjs"; +export * from "./helpers/parseUtil.cjs"; +export * from "./helpers/typeAliases.cjs"; +export * from "./helpers/util.cjs"; +export * from "./types.cjs"; +export * from "./ZodError.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts new file mode 100644 index 000000000..f34a63d7f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.cjs"; +import type { ZodParsedType } from "./util.cjs"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts new file mode 100644 index 000000000..3f9e46a2a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.cjs"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/node_modules/zod/v3/index.d.cts b/tests/fixtures/npm/node_modules/zod/v3/index.d.cts new file mode 100644 index 000000000..b0662babe --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/index.d.cts @@ -0,0 +1,4 @@ +import * as z from "./external.cjs"; +export * from "./external.cjs"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts b/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts new file mode 100644 index 000000000..e146a11c5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.cjs"; +declare const errorMap: ZodErrorMap; +export = errorMap; diff --git a/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts b/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/types.d.cts b/tests/fixtures/npm/node_modules/zod/v3/types.d.cts new file mode 100644 index 000000000..f1013a74f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/types.d.cts @@ -0,0 +1,1034 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.cjs"; +import type { enumUtil } from "./helpers/enumUtil.cjs"; +import { errorUtil } from "./helpers/errorUtil.cjs"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.cjs"; +import type { partialUtil } from "./helpers/partialUtil.cjs"; +import type { Primitive } from "./helpers/typeAliases.cjs"; +import { util, type objectUtil } from "./helpers/util.cjs"; +import type { StandardSchemaV1 } from "./standard-schema.cjs"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + _cached: { + shape: T; + keys: string[]; + } | null; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts new file mode 100644 index 000000000..757d60e6e --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts @@ -0,0 +1 @@ +export { _lt as lt, _lte as lte, _gt as gt, _gte as gte, _positive as positive, _negative as negative, _nonpositive as nonpositive, _nonnegative as nonnegative, _multipleOf as multipleOf, _maxSize as maxSize, _minSize as minSize, _size as size, _maxLength as maxLength, _minLength as minLength, _length as length, _regex as regex, _lowercase as lowercase, _uppercase as uppercase, _includes as includes, _startsWith as startsWith, _endsWith as endsWith, _property as property, _properties as properties, _mime as mime, _overwrite as overwrite, _normalize as normalize, _trim as trim, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, _slugify as slugify, type $RefinementCtx as RefinementCtx, } from "../core/index.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js new file mode 100644 index 000000000..50a08b884 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js @@ -0,0 +1 @@ +export { _lt as lt, _lte as lte, _gt as gt, _gte as gte, _positive as positive, _negative as negative, _nonpositive as nonpositive, _nonnegative as nonnegative, _multipleOf as multipleOf, _maxSize as maxSize, _minSize as minSize, _size as size, _maxLength as maxLength, _minLength as minLength, _length as length, _regex as regex, _lowercase as lowercase, _uppercase as uppercase, _includes as includes, _startsWith as startsWith, _endsWith as endsWith, _property as property, _properties as properties, _mime as mime, _overwrite as overwrite, _normalize as normalize, _trim as trim, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, _slugify as slugify, } from "../core/index.js"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts new file mode 100644 index 000000000..231b91745 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts @@ -0,0 +1,17 @@ +import * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +export interface ZodCoercedString extends schemas._ZodString> { +} +export declare function string(params?: string | core.$ZodStringParams): ZodCoercedString; +export interface ZodCoercedNumber extends schemas._ZodNumber> { +} +export declare function number(params?: string | core.$ZodNumberParams): ZodCoercedNumber; +export interface ZodCoercedBoolean extends schemas._ZodBoolean> { +} +export declare function boolean(params?: string | core.$ZodBooleanParams): ZodCoercedBoolean; +export interface ZodCoercedBigInt extends schemas._ZodBigInt> { +} +export declare function bigint(params?: string | core.$ZodBigIntParams): ZodCoercedBigInt; +export interface ZodCoercedDate extends schemas._ZodDate> { +} +export declare function date(params?: string | core.$ZodDateParams): ZodCoercedDate; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js new file mode 100644 index 000000000..141cf8936 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js @@ -0,0 +1,17 @@ +import * as core from "../core/index.js"; +import * as schemas from "./schemas.js"; +export function string(params) { + return core._coercedString(schemas.ZodString, params); +} +export function number(params) { + return core._coercedNumber(schemas.ZodNumber, params); +} +export function boolean(params) { + return core._coercedBoolean(schemas.ZodBoolean, params); +} +export function bigint(params) { + return core._coercedBigint(schemas.ZodBigInt, params); +} +export function date(params) { + return core._coercedDate(schemas.ZodDate, params); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts new file mode 100644 index 000000000..8f1b3d8c8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts @@ -0,0 +1,48 @@ +import * as core from "../core/index.cjs"; +import type { ZodType } from "./schemas.cjs"; +/** @deprecated Use `z.output` instead. */ +export type TypeOf = core.output; +/** @deprecated Use `z.output` instead. */ +export type Infer = core.output; +/** @deprecated Use `z.core.$ZodTypes` instead */ +export type ZodFirstPartySchemaTypes = core.$ZodTypes; +/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */ +export declare const ZodIssueCode: { + readonly invalid_type: "invalid_type"; + readonly too_big: "too_big"; + readonly too_small: "too_small"; + readonly invalid_format: "invalid_format"; + readonly not_multiple_of: "not_multiple_of"; + readonly unrecognized_keys: "unrecognized_keys"; + readonly invalid_union: "invalid_union"; + readonly invalid_key: "invalid_key"; + readonly invalid_element: "invalid_element"; + readonly invalid_value: "invalid_value"; + readonly custom: "custom"; +}; +/** @deprecated Use `z.core.$ZodFlattenedError>` instead; it takes an inferred type, not a schema. */ +export type inferFlattenedErrors = core.$ZodFlattenedError, U>; +/** @deprecated Use `z.core.$ZodFormattedError>` instead; it takes an inferred type, not a schema. */ +export type inferFormattedError, U = string> = core.$ZodFormattedError, U>; +/** Use `z.$brand` instead */ +export type BRAND = { + [core.$brand]: { + [k in T]: true; + }; +}; +export { $brand, config } from "../core/index.cjs"; +/** @deprecated Use `z.config(params)` instead. */ +export declare function setErrorMap(map: core.$ZodErrorMap): void; +/** @deprecated Use `z.config()` instead. */ +export declare function getErrorMap(): core.$ZodErrorMap | undefined; +/** @deprecated Use z.ZodType (without generics) instead. */ +export type ZodTypeAny = core.$ZodTypeInternals> = ZodType; +/** @deprecated Use `z.ZodType` */ +export type ZodSchema = core.$ZodTypeInternals> = ZodType; +/** @deprecated Use `z.ZodType` */ +export type Schema = core.$ZodTypeInternals> = ZodType; +/** Included for Zod 3 compatibility */ +export type ZodRawShape = core.$ZodShape; +/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */ +export declare enum ZodFirstPartyTypeKind { +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js new file mode 100644 index 000000000..2179ea904 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js @@ -0,0 +1,31 @@ +// Zod 3 compat layer +import * as core from "../core/index.js"; +/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */ +export const ZodIssueCode = { + invalid_type: "invalid_type", + too_big: "too_big", + too_small: "too_small", + invalid_format: "invalid_format", + not_multiple_of: "not_multiple_of", + unrecognized_keys: "unrecognized_keys", + invalid_union: "invalid_union", + invalid_key: "invalid_key", + invalid_element: "invalid_element", + invalid_value: "invalid_value", + custom: "custom", +}; +export { $brand, config } from "../core/index.js"; +/** @deprecated Use `z.config(params)` instead. */ +export function setErrorMap(map) { + core.config({ + customError: map, + }); +} +/** @deprecated Use `z.config()` instead. */ +export function getErrorMap() { + return core.config().customError; +} +/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */ +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts new file mode 100644 index 000000000..674ecf18e --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts @@ -0,0 +1,14 @@ +import type * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +/** Distributes `.partial()` through every object in a schema type, preserving the wrappers around them. */ +export type DeepPartial = T extends schemas.ZodObject ? schemas.ZodObject<{ + -readonly [K in keyof Shape]: schemas.ZodOptional>; +}, Config> : T extends schemas.ZodDiscriminatedUnion ? schemas.ZodUnion<{ + -readonly [I in keyof Options]: Options[I] extends core.SomeType ? DeepPartial : Options[I]; +}> : T extends schemas.ZodUnion ? schemas.ZodUnion<{ + -readonly [I in keyof Options]: Options[I] extends core.SomeType ? DeepPartial : Options[I]; +}> : T extends schemas.ZodArray ? schemas.ZodArray> : T extends schemas.ZodTuple ? schemas.ZodTuple<{ + -readonly [K in keyof Items]: Items[K] extends core.SomeType ? DeepPartial : Items[K]; +}, Rest extends core.SomeType ? DeepPartial : Rest> : T extends schemas.ZodIntersection ? schemas.ZodIntersection, DeepPartial> : T extends schemas.ZodRecord ? schemas.ZodRecord> : T extends schemas.ZodMap ? schemas.ZodMap> : T extends schemas.ZodSet ? schemas.ZodSet> : T extends schemas.ZodOptional ? schemas.ZodOptional> : T extends schemas.ZodNullable ? schemas.ZodNullable> : T extends schemas.ZodDefault ? schemas.ZodDefault> : T extends schemas.ZodPrefault ? schemas.ZodPrefault> : T extends schemas.ZodNonOptional ? schemas.ZodNonOptional> : T extends schemas.ZodCatch ? schemas.ZodCatch> : T extends schemas.ZodReadonly ? schemas.ZodReadonly> : T extends schemas.ZodSuccess ? schemas.ZodSuccess> : T extends schemas.ZodPromise ? schemas.ZodPromise> : T extends schemas.ZodCodec ? schemas.ZodCodec, DeepPartial> : T extends schemas.ZodPreprocess ? schemas.ZodPreprocess> : T extends schemas.ZodPipe ? schemas.ZodPipe, DeepPartial> : T extends schemas.ZodLazy ? schemas.ZodLazy> : T; +/** Returns a copy of the schema with every nested object's properties made optional. */ +export declare function deepPartial(schema: T): DeepPartial; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js new file mode 100644 index 000000000..a87bb625c --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js @@ -0,0 +1,13 @@ +import { visit } from "../core/visit.js"; +import * as schemas from "./schemas.js"; +/** Returns a copy of the schema with every nested object's properties made optional. */ +export function deepPartial(schema) { + return visit(schema, { + object: (s) => s.partial(), + // Every partialed option now admits `undefined`, which the constructor rejects as a duplicate. + union: (s) => { + const def = s._zod.def; + return def.discriminator === undefined ? s : schemas.union(def.options); + }, + }); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts new file mode 100644 index 000000000..472102f6a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts @@ -0,0 +1,29 @@ +import * as core from "../core/index.cjs"; +import { $ZodError } from "../core/index.cjs"; +/** @deprecated Use `z.core.$ZodIssue` from `@zod/core` instead, especially if you are building a library on top of Zod. */ +export type ZodIssue = core.$ZodIssue; +/** An Error-like class used to store Zod validation issues. */ +export interface ZodError extends $ZodError { + /** @deprecated Use the `z.treeifyError(err)` function instead. */ + format(): core.$ZodFormattedError; + format(mapper: (issue: core.$ZodIssue) => U): core.$ZodFormattedError; + /** @deprecated Use the `z.treeifyError(err)` function instead. */ + flatten(): core.$ZodFlattenedError; + flatten(mapper: (issue: core.$ZodIssue) => U): core.$ZodFlattenedError; + /** @deprecated Push directly to `.issues` instead. */ + addIssue(issue: core.$ZodIssue): void; + /** @deprecated Push directly to `.issues` instead. */ + addIssues(issues: core.$ZodIssue[]): void; + /** @deprecated Check `err.issues.length === 0` instead. */ + isEmpty: boolean; +} +export declare const ZodError: core.$constructor; +export declare const ZodRealError: core.$constructor; +/** @deprecated Use `z.core.$ZodFlattenedError` instead. */ +export type ZodFlattenedError = core.$ZodFlattenedError; +/** @deprecated Use `z.core.$ZodFormattedError` instead. */ +export type ZodFormattedError = core.$ZodFormattedError; +/** @deprecated Use `z.core.$ZodErrorMap` instead. */ +export type ZodErrorMap = core.$ZodErrorMap; +/** @deprecated Use `z.core.$ZodRawIssue` instead. */ +export type IssueData = core.$ZodRawIssue; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js new file mode 100644 index 000000000..e3c02587a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js @@ -0,0 +1,57 @@ +import * as core from "../core/index.js"; +import { $ZodError } from "../core/index.js"; +import * as util from "../core/util.js"; +/* Prototypes that already carry the lazy helper methods. Seeded with the + * intrinsics so that `init` on a foreign object — it accepts any object — + * can never install an accessor onto a prototype we do not own. */ +const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]); +/* Helper methods live as non-enumerable lazy getters on the shared + * prototype instead of own properties on every instance. On first + * access the getter allocates the per-instance closure and caches it + * as a non-enumerable own property, so detached usage still works and + * the allocation only happens for methods actually touched. */ +function _lazyMethod(proto, key, make) { + Object.defineProperty(proto, key, { + configurable: true, + enumerable: false, + get() { + const value = make(this); + Object.defineProperty(this, key, { value, configurable: true, writable: true }); + return value; + }, + set(value) { + Object.defineProperty(this, key, { value, configurable: true, writable: true }); + }, + }); +} +const initializer = (inst, issues) => { + $ZodError.init(inst, issues); + inst.name = "ZodError"; + const proto = Object.getPrototypeOf(inst); + if (_installedErrorProtos.has(proto)) + return; + _installedErrorProtos.add(proto); + _lazyMethod(proto, "format", (self) => (mapper) => core.formatError(self, mapper)); + _lazyMethod(proto, "flatten", (self) => (mapper) => core.flattenError(self, mapper)); + _lazyMethod(proto, "addIssue", (self) => (issue) => { + self.issues.push(issue); + self.message = JSON.stringify(self.issues, util.jsonStringifyReplacer, 2); + }); + _lazyMethod(proto, "addIssues", (self) => (issues) => { + self.issues.push(...issues); + self.message = JSON.stringify(self.issues, util.jsonStringifyReplacer, 2); + }); + Object.defineProperty(proto, "isEmpty", { + configurable: true, + enumerable: false, + get() { + return this.issues.length === 0; + }, + }); +}; +export const ZodError = /*@__PURE__*/ core.$constructor("ZodError", initializer); +export const ZodRealError = /*@__PURE__*/ core.$constructor("ZodError", initializer, undefined, { + Parent: Error, +}); +// /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */ +// export type ErrorMapCtx = core.$ZodErrorMapCtx; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts new file mode 100644 index 000000000..7e9ce5e74 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts @@ -0,0 +1,19 @@ +export * as core from "../core/index.cjs"; +export * from "./schemas.cjs"; +export * from "./checks.cjs"; +export * from "./errors.cjs"; +export * from "./parse.cjs"; +export * from "./compat.cjs"; +export type { infer } from "../core/index.cjs"; +export type { JSONType } from "../core/util.cjs"; +export type { CompileOptions } from "../core/index.cjs"; +export { globalRegistry, type GlobalMeta, registry, config, memoizer, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, TimePrecision, util, NEVER, INVALID, toZod, compile, withParser, ZodCompileAsyncError, ZodCompileUnsupportedError, getDiscriminatedOption, } from "../core/index.cjs"; +export { toJSONSchema } from "../core/json-schema-processors.cjs"; +export { fromJSONSchema } from "./from-json-schema.cjs"; +export { deepPartial } from "./deep-partial.cjs"; +export { input, output } from "./in-out.cjs"; +export * as locales from "../locales/index.cjs"; +export { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from "./iso.cjs"; +export * as iso from "./iso.cjs"; +export type { ZodCoercedString, ZodCoercedNumber, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, } from "./coerce.cjs"; +export * as coerce from "./coerce.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/external.js b/tests/fixtures/npm/node_modules/zod/v4/classic/external.js new file mode 100644 index 000000000..435be167f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/external.js @@ -0,0 +1,17 @@ +export * as core from "../core/index.js"; +export * from "./schemas.js"; +export * from "./checks.js"; +export * from "./errors.js"; +export * from "./parse.js"; +export * from "./compat.js"; +export { globalRegistry, registry, config, memoizer, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, TimePrecision, util, NEVER, INVALID, toZod, compile, withParser, ZodCompileAsyncError, ZodCompileUnsupportedError, getDiscriminatedOption, } from "../core/index.js"; +export { toJSONSchema } from "../core/json-schema-processors.js"; +export { fromJSONSchema } from "./from-json-schema.js"; +export { deepPartial } from "./deep-partial.js"; +// Types and values both, from one module — re-exporting the types from core would collide. +export { input, output } from "./in-out.js"; +export * as locales from "../locales/index.js"; +// iso must be exported from top-level https://github.com/colinhacks/zod/issues/4491 +export { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from "./iso.js"; +export * as iso from "./iso.js"; +export * as coerce from "./coerce.js"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts new file mode 100644 index 000000000..dc028bc01 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts @@ -0,0 +1,12 @@ +import type * as JSONSchema from "../core/json-schema.cjs"; +import { type $ZodRegistry } from "../core/registries.cjs"; +import type { ZodType } from "./schemas.cjs"; +type JSONSchemaVersion = "draft-2020-12" | "draft-7" | "draft-4" | "openapi-3.0"; +interface FromJSONSchemaParams { + defaultTarget?: JSONSchemaVersion; + registry?: $ZodRegistry; +} +/** + * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */ +export declare function fromJSONSchema(schema: JSONSchema.JSONSchema | boolean, params?: FromJSONSchemaParams): ZodType; +export {}; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js new file mode 100644 index 000000000..1f79b5c85 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js @@ -0,0 +1,881 @@ +import { globalRegistry } from "../core/registries.js"; +import { assignProp, isPlainObject } from "../core/util.js"; +import * as _checks from "./checks.js"; +import * as _iso from "./iso.js"; +import * as _schemas from "./schemas.js"; +// Local z object to avoid circular dependency with ../index.js +const z = { + ..._schemas, + ..._checks, + iso: _iso, +}; +// Keys that are recognized and handled by the conversion logic +const RECOGNIZED_KEYS = /*@__PURE__*/ new Set([ + // Schema identification + "$schema", + "$ref", + "$defs", + "definitions", + // Core schema keywords + "$id", + "id", + "$comment", + "$anchor", + "$vocabulary", + "$dynamicRef", + "$dynamicAnchor", + // Type + "type", + "enum", + "const", + // Composition + "anyOf", + "oneOf", + "allOf", + "not", + // Object + "properties", + "required", + "additionalProperties", + "patternProperties", + "propertyNames", + "minProperties", + "maxProperties", + // Array + "items", + "prefixItems", + "additionalItems", + "minItems", + "maxItems", + "uniqueItems", + "contains", + "minContains", + "maxContains", + // String + "minLength", + "maxLength", + "pattern", + "format", + // Number + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + // Already handled metadata + "description", + "default", + // Content + "contentEncoding", + "contentMediaType", + "contentSchema", + // Unsupported (error-throwing) + "unevaluatedItems", + "unevaluatedProperties", + "if", + "then", + "else", + "dependentSchemas", + "dependentRequired", + // OpenAPI + "nullable", + "readOnly", +]); +function detectVersion(schema, defaultTarget) { + const $schema = schema.$schema; + if ($schema === "https://json-schema.org/draft/2020-12/schema") { + return "draft-2020-12"; + } + if ($schema === "http://json-schema.org/draft-07/schema#") { + return "draft-7"; + } + if ($schema === "http://json-schema.org/draft-04/schema#") { + return "draft-4"; + } + // Use defaultTarget if provided, otherwise default to draft-2020-12 + return defaultTarget ?? "draft-2020-12"; +} +// Positional schemas constrain the elements that are present; only minItems makes them required. +function applyMinItems(items, minItems) { + return items.map((item, index) => (index < minItems ? item : item.optional())); +} +// Inverse of the encoding applied to $ref pointer segments in to-json-schema.ts. Per RFC 6901 the `~1` replacement must run before `~0`. +function decodeJSONPointerSegment(segment) { + return segment.replace(/~1/g, "/").replace(/~0/g, "~"); +} +function resolveRef(ref, ctx) { + if (!ref.startsWith("#")) { + throw new Error("External $ref is not supported, only local refs (#/...) are allowed"); + } + const path = ref.slice(1).split("/").filter(Boolean); + // Handle root reference "#" + if (path.length === 0) { + return ctx.rootSchema; + } + const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions"; + if (path[0] === defsKey) { + const key = path[1] === undefined ? undefined : decodeJSONPointerSegment(path[1]); + if (!key || !ctx.defs[key]) { + throw new Error(`Reference not found: ${ref}`); + } + return ctx.defs[key]; + } + throw new Error(`Reference not found: ${ref}`); +} +/** + * Enforces the keywords that constrain an object's own keys — `propertyNames`, + * `minProperties`, `maxProperties` — before `objectSchema` runs. The guard has + * to see the raw input: an object parse drops `__proto__` and can add keys from + * a property `default`, so its output is not the set of names the instance + * actually carried. + */ +function checkObjectGuards(objectSchema, guards) { + // An identity transform, not z.any(), so `toJSONSchema` reports the object on both the input and the output side of the pipe. + const guard = z + .transform((value) => value) + .check((payload) => { + const value = payload.value; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return; + const keys = Object.getOwnPropertyNames(value); + if (guards.minProperties !== undefined && keys.length < guards.minProperties) { + payload.issues.push({ + origin: "object", + code: "too_small", + minimum: guards.minProperties, + inclusive: true, + message: `Too small: expected object to have >=${guards.minProperties} properties`, + input: value, + inst: objectSchema, + continue: true, + }); + } + if (guards.maxProperties !== undefined && keys.length > guards.maxProperties) { + payload.issues.push({ + origin: "object", + code: "too_big", + maximum: guards.maxProperties, + inclusive: true, + message: `Too big: expected object to have <=${guards.maxProperties} properties`, + input: value, + inst: objectSchema, + continue: true, + }); + } + if (guards.keySchema) { + for (const key of keys) { + const result = guards.keySchema.safeParse(key); + if (result.success) + continue; + payload.issues.push({ + code: "invalid_key", + origin: "record", + issues: result.error.issues, + input: key, + path: [key], + continue: true, + }); + } + } + }); + return guard.pipe(objectSchema); +} +/** + * An injective string for JSON data: two values share a key exactly when `uniqueItems` calls them + * equal, so duplicate detection is a Map lookup rather than a pairwise walk. Strings and keys are + * length-prefixed so no value can spell another one. `null` means "never equal to anything" — a + * cycle or a NaN, neither of which JSON can express. + */ +function canonicalKey(value, seen) { + if (value === null) + return "z"; + const type = typeof value; + if (type !== "object") { + if (type === "number" && Number.isNaN(value)) + return null; + const raw = String(value); + return `${type[0]}${raw.length}:${raw}`; + } + if (seen.has(value)) + return null; + seen.add(value); + try { + if (Array.isArray(value)) { + const parts = []; + for (const item of value) { + const key = canonicalKey(item, seen); + if (key === null) + return null; + parts.push(key); + } + return `a${parts.length}:[${parts.join(",")}]`; + } + const keys = Object.keys(value).sort(); + const parts = []; + for (const k of keys) { + const key = canonicalKey(value[k], seen); + if (key === null) + return null; + parts.push(`${k.length}:${k}=${key}`); + } + return `o${parts.length}:{${parts.join(",")}}`; + } + finally { + seen.delete(value); + } +} +// keywords whose value is a schema or an array of them +const SCHEMA_KEYWORDS = /*@__PURE__*/ new Set([ + "items", + "prefixItems", + "additionalItems", + "additionalProperties", + "contains", + "propertyNames", + "not", + "if", + "then", + "else", + "allOf", + "anyOf", + "oneOf", + "unevaluatedItems", + "unevaluatedProperties", + "contentSchema", +]); +// keywords whose value maps names to schemas. draft-7 `dependencies` also allows an array of property names, which holds no schema to walk +const SCHEMA_MAP_KEYWORDS = /*@__PURE__*/ new Set([ + "properties", + "patternProperties", + "dependentSchemas", + "dependencies", + "$defs", + "definitions", +]); +/** + * True when a subschema holds a `$ref` anywhere a schema can appear. Only those positions are + * walked: `$ref` under `default` or an `x-` annotation is instance data with an ordinary key, and + * reading it as a reference would drop a constraint that resolves perfectly well. + */ +function containsRef(value) { + if (typeof value !== "object" || value === null) + return false; + if (Array.isArray(value)) + return value.some(containsRef); + if (typeof value.$ref === "string") + return true; + return Object.entries(value).some(([key, sub]) => { + if (SCHEMA_KEYWORDS.has(key)) + return containsRef(sub); + if (!SCHEMA_MAP_KEYWORDS.has(key) || typeof sub !== "object" || sub === null) + return false; + return Object.values(sub).some(containsRef); + }); +} +function plural(n) { + return n === 1 ? "element" : "elements"; +} +/** + * Enforces `uniqueItems` and the `contains` family before `arraySchema` runs, for the same reason + * `checkObjectGuards` runs pre-pipe: an item parse can apply a nested `default`, so the parsed + * array is not the instance the keywords are defined over. A guard issue aborts the pipe, so + * `minItems`/`maxItems` are not also reported on the same parse — instance correctness is worth + * more than co-reporting two issues. + */ +function checkArrayGuards(arraySchema, guards) { + // An identity transform, not z.any(), so `toJSONSchema` reports the array on both the input and the output side of the pipe. + const guard = z + .transform((value) => value) + .check((payload) => { + const items = payload.value; + if (!Array.isArray(items)) + return; + if (guards.uniqueItems === true) { + const firstSeen = new Map(); + for (let i = 0; i < items.length; i++) { + const key = canonicalKey(items[i], new Set()); + if (key === null) + continue; + const first = firstSeen.get(key); + if (first === undefined) { + firstSeen.set(key, i); + continue; + } + payload.issues.push({ + code: "custom", + message: `Array items must be unique: element at index ${i} duplicates the one at index ${first}`, + input: items, + path: [i], + continue: true, + }); + } + } + if (guards.containsSchema) { + const minContains = guards.minContains ?? 1; + // one past the ceiling already proves the failure, and nothing above it changes the verdict + const ceiling = guards.maxContains !== undefined ? guards.maxContains + 1 : Number.POSITIVE_INFINITY; + let matches = 0; + for (const item of items) { + if (guards.containsSchema.safeParse(item).success && ++matches >= ceiling) + break; + } + if (matches < minContains) { + // the scan ran to the end, so this count is exact + payload.issues.push({ + code: "custom", + message: `Array must contain at least ${minContains} matching ${plural(minContains)}; found ${matches}`, + input: items, + continue: true, + }); + } + if (guards.maxContains !== undefined && matches > guards.maxContains) { + payload.issues.push({ + code: "custom", + message: `Array must contain at most ${guards.maxContains} matching ${plural(guards.maxContains)}`, + input: items, + continue: true, + }); + } + } + }); + return guard.pipe(arraySchema); +} +function getTupleRest(restSchema, ctx) { + if (restSchema === false) { + return undefined; + } + if (restSchema === undefined || restSchema === true) { + return z.any(); + } + return convertSchema(restSchema, ctx); +} +// the RFC 3339 full-time keyword, narrower than `z.iso.time()`; local because sharing it via `timeSource` pins that builder into every bundle (+139 B gzipped on a mini `z.boolean()`) +const fullTime = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/; +function convertBaseSchema(schema, ctx) { + // Handle unsupported features + if (schema.not !== undefined) { + // Special case: { not: {} } represents never + if (typeof schema.not === "object" && Object.keys(schema.not).length === 0) { + return z.never(); + } + throw new Error("not is not supported in Zod (except { not: {} } for never)"); + } + if (schema.unevaluatedItems !== undefined) { + throw new Error("unevaluatedItems is not supported"); + } + if (schema.unevaluatedProperties !== undefined) { + throw new Error("unevaluatedProperties is not supported"); + } + if (schema.if !== undefined || schema.then !== undefined || schema.else !== undefined) { + throw new Error("Conditional schemas (if/then/else) are not supported"); + } + if (schema.dependentSchemas !== undefined || schema.dependentRequired !== undefined) { + throw new Error("dependentSchemas and dependentRequired are not supported"); + } + // Handle $ref + if (schema.$ref) { + const refPath = schema.$ref; + if (ctx.refs.has(refPath)) { + return ctx.refs.get(refPath); + } + if (ctx.processing.has(refPath)) { + // Circular reference - use lazy + return z.lazy(() => { + if (!ctx.refs.has(refPath)) { + throw new Error(`Circular reference not resolved: ${refPath}`); + } + return ctx.refs.get(refPath); + }); + } + ctx.processing.add(refPath); + const resolved = resolveRef(refPath, ctx); + const zodSchema = convertSchema(resolved, ctx); + ctx.refs.set(refPath, zodSchema); + ctx.processing.delete(refPath); + return zodSchema; + } + // Handle enum + if (schema.enum !== undefined) { + const enumValues = schema.enum; + // Special case: OpenAPI 3.0 null representation { type: "string", nullable: true, enum: [null] } + if (ctx.version === "openapi-3.0" && + schema.nullable === true && + enumValues.length === 1 && + enumValues[0] === null) { + return z.null(); + } + if (enumValues.length === 0) { + return z.never(); + } + if (enumValues.length === 1) { + return z.literal(enumValues[0]); + } + // Check if all values are strings + if (enumValues.every((v) => typeof v === "string")) { + return z.enum(enumValues); + } + // Mixed types - use union of literals + const literalSchemas = enumValues.map((v) => z.literal(v)); + if (literalSchemas.length < 2) { + return literalSchemas[0]; + } + return z.union([literalSchemas[0], literalSchemas[1], ...literalSchemas.slice(2)]); + } + // Handle const + if (schema.const !== undefined) { + return z.literal(schema.const); + } + // Handle type + const type = schema.type; + if (Array.isArray(type)) { + // Expand type array into anyOf union + const typeSchemas = type.map((t) => { + const typeSchema = { ...schema, type: t }; + return convertBaseSchema(typeSchema, ctx); + }); + if (typeSchemas.length === 0) { + return z.never(); + } + if (typeSchemas.length === 1) { + return typeSchemas[0]; + } + return z.union(typeSchemas); + } + if (!type) { + // No type specified - empty schema (any) + return z.any(); + } + let zodSchema; + switch (type) { + case "string": { + let stringSchema = z.string(); + // Apply format using .check() with Zod format functions + if (schema.format) { + const format = schema.format; + // Map common formats to Zod check functions + if (format === "email") { + stringSchema = stringSchema.check(z.email()); + } + else if (format === "uri" || format === "uri-reference") { + stringSchema = stringSchema.check(z.url()); + } + else if (format === "uuid" || format === "guid") { + stringSchema = stringSchema.check(z.uuid()); + } + else if (format === "date-time") { + stringSchema = stringSchema.check(z.iso.datetime({ offset: true })); + } + else if (format === "date") { + stringSchema = stringSchema.check(z.iso.date()); + } + else if (format === "time") { + stringSchema = stringSchema.check(z.regex(fullTime)); + } + else if (format === "duration") { + stringSchema = stringSchema.check(z.iso.duration()); + } + else if (format === "hostname") { + stringSchema = stringSchema.check(z.hostname()); + } + else if (format === "ipv4") { + stringSchema = stringSchema.check(z.ipv4()); + } + else if (format === "ipv6") { + stringSchema = stringSchema.check(z.ipv6()); + } + else if (format === "mac") { + stringSchema = stringSchema.check(z.mac()); + } + else if (format === "cidr") { + stringSchema = stringSchema.check(z.cidrv4()); + } + else if (format === "cidr-v6") { + stringSchema = stringSchema.check(z.cidrv6()); + } + else if (format === "base64") { + stringSchema = stringSchema.check(z.base64()); + } + else if (format === "base64url") { + stringSchema = stringSchema.check(z.base64url()); + } + else if (format === "e164") { + stringSchema = stringSchema.check(z.e164()); + } + else if (format === "credit_card") { + stringSchema = stringSchema.check(z.creditCard()); + } + else if (format === "iban") { + stringSchema = stringSchema.check(z.iban()); + } + else if (format === "jwt") { + stringSchema = stringSchema.check(z.jwt()); + } + else if (format === "emoji") { + stringSchema = stringSchema.check(z.emoji()); + } + else if (format === "nanoid") { + stringSchema = stringSchema.check(z.nanoid()); + } + else if (format === "cuid") { + stringSchema = stringSchema.check(z.cuid()); + } + else if (format === "cuid2") { + stringSchema = stringSchema.check(z.cuid2()); + } + else if (format === "ulid") { + stringSchema = stringSchema.check(z.ulid()); + } + else if (format === "xid") { + stringSchema = stringSchema.check(z.xid()); + } + else if (format === "ksuid") { + stringSchema = stringSchema.check(z.ksuid()); + } + // Note: json-string format is not currently supported by Zod + // Custom formats are ignored - keep as plain string + } + // Apply constraints + if (typeof schema.minLength === "number") { + stringSchema = stringSchema.min(schema.minLength); + } + if (typeof schema.maxLength === "number") { + stringSchema = stringSchema.max(schema.maxLength); + } + if (schema.pattern) { + // JSON Schema patterns are not implicitly anchored (match anywhere in string) + stringSchema = stringSchema.regex(new RegExp(schema.pattern)); + } + zodSchema = stringSchema; + break; + } + case "number": + case "integer": { + let numberSchema = type === "integer" ? z.number().int() : z.number(); + // Apply constraints + // In draft-04, `exclusiveMinimum: true` makes the sibling `minimum` exclusive rather than an independent bound, so the inclusive `.min()` is skipped; emitting it too would be dominated by the exclusive check and report a second, weaker issue. + if (typeof schema.minimum === "number" && schema.exclusiveMinimum !== true) { + numberSchema = numberSchema.min(schema.minimum); + } + if (typeof schema.maximum === "number" && schema.exclusiveMaximum !== true) { + numberSchema = numberSchema.max(schema.maximum); + } + if (typeof schema.exclusiveMinimum === "number") { + numberSchema = numberSchema.gt(schema.exclusiveMinimum); + } + else if (schema.exclusiveMinimum === true && typeof schema.minimum === "number") { + numberSchema = numberSchema.gt(schema.minimum); + } + if (typeof schema.exclusiveMaximum === "number") { + numberSchema = numberSchema.lt(schema.exclusiveMaximum); + } + else if (schema.exclusiveMaximum === true && typeof schema.maximum === "number") { + numberSchema = numberSchema.lt(schema.maximum); + } + if (typeof schema.multipleOf === "number") { + numberSchema = numberSchema.multipleOf(schema.multipleOf); + } + zodSchema = numberSchema; + break; + } + case "boolean": { + zodSchema = z.boolean(); + break; + } + case "null": { + zodSchema = z.null(); + break; + } + case "object": { + const shape = {}; + const properties = schema.properties || {}; + const requiredSet = new Set(schema.required || []); + const additionalSchema = typeof schema.additionalProperties === "object" + ? convertSchema(schema.additionalProperties, ctx) + : undefined; + // Convert properties - mark optional ones + for (const [key, propSchema] of Object.entries(properties)) { + const propZodSchema = convertSchema(propSchema, ctx); + // If not in required array, make it optional. assignProp so a __proto__ key becomes an own property instead of hitting the inherited setter + assignProp(shape, key, requiredSet.has(key) ? propZodSchema : propZodSchema.optional()); + } + // Handle patternProperties + if (schema.patternProperties) { + // patternProperties: keys matching pattern must satisfy corresponding schema. Use loose records so non-matching keys pass through + const patternProps = schema.patternProperties; + const patternKeys = Object.keys(patternProps); + const looseRecords = []; + for (const pattern of patternKeys) { + const patternValue = convertSchema(patternProps[pattern], ctx); + const keySchema = z.string().regex(new RegExp(pattern)); + looseRecords.push(z.looseRecord(keySchema, patternValue)); + } + // Build intersection: object schema + all pattern property records + const schemasToIntersect = []; + if (Object.keys(shape).length > 0) { + // Use passthrough so patternProperties can validate additional keys + schemasToIntersect.push(z.object(shape).passthrough()); + } + schemasToIntersect.push(...looseRecords); + if (schemasToIntersect.length === 0) { + zodSchema = z.object({}).passthrough(); + } + else if (schemasToIntersect.length === 1) { + zodSchema = schemasToIntersect[0]; + } + else { + // Chain intersections: (A & B) & C & D ... + let result = z.intersection(schemasToIntersect[0], schemasToIntersect[1]); + for (let i = 2; i < schemasToIntersect.length; i++) { + result = z.intersection(result, schemasToIntersect[i]); + } + zodSchema = result; + } + // When additionalProperties is false, reject keys that are neither defined in properties nor matched by any patternProperty. + if (schema.additionalProperties === false) { + const propertyKeys = Object.keys(shape); + const patterns = patternKeys.map((p) => new RegExp(p)); + const basePatternSchema = zodSchema; + zodSchema = zodSchema.check((payload) => { + if (!isPlainObject(payload.value)) + return; + const unrecognized = []; + for (const key of Object.keys(payload.value)) { + if (propertyKeys.includes(key)) + continue; + if (patterns.some((regex) => regex.test(key))) + continue; + unrecognized.push(key); + } + if (unrecognized.length) { + payload.issues.push({ + code: "unrecognized_keys", + keys: unrecognized, + input: payload.value, + inst: basePatternSchema, + }); + } + }); + } + } + else { + // Handle additionalProperties. In JSON Schema, additionalProperties defaults to true (allow any extra properties). In Zod, objects strip unknown keys by default, so we need to handle this explicitly + const objectSchema = z.object(shape); + if (schema.additionalProperties === false) { + // Strict mode - no extra properties allowed + zodSchema = objectSchema.strict(); + } + else if (additionalSchema) { + // Extra properties must match the specified schema + zodSchema = objectSchema.catchall(additionalSchema); + } + else { + // additionalProperties is true or undefined - allow any extra properties (passthrough) + zodSchema = objectSchema.passthrough(); + } + } + // propertyNames constrains key *names* only, and says nothing about which keys are required or how their values validate. Layering it on top of the result keeps properties/patternProperties/additionalProperties composing underneath. `true` allows every name, so it needs no guard. + const hasKeyGuard = schema.propertyNames !== undefined && schema.propertyNames !== true; + const minProperties = typeof schema.minProperties === "number" ? schema.minProperties : undefined; + const maxProperties = typeof schema.maxProperties === "number" ? schema.maxProperties : undefined; + if (hasKeyGuard || minProperties !== undefined || maxProperties !== undefined) { + let keySchema; + if (hasKeyGuard) { + // Keys are always strings, so a propertyNames subschema that omits `type` still constrains them — without this it would convert to z.any(). + const keyJSONSchema = typeof schema.propertyNames === "object" && schema.propertyNames.type === undefined + ? { type: "string", ...schema.propertyNames } + : schema.propertyNames; + keySchema = convertSchema(keyJSONSchema, ctx); + } + zodSchema = checkObjectGuards(zodSchema, { keySchema, minProperties, maxProperties }); + } + break; + } + case "array": { + // Check if this is a tuple (prefixItems or items as array) + const prefixItems = schema.prefixItems; + const items = schema.items; + if (prefixItems && Array.isArray(prefixItems)) { + // Tuple with prefixItems (draft-2020-12) + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0; + const tupleItems = prefixItems.map((item) => convertSchema(item, ctx)); + const positionalItems = applyMinItems(tupleItems, minItems); + const rest = !Array.isArray(items) ? getTupleRest(items, ctx) : undefined; + const tupleSchema = z.tuple(positionalItems); + zodSchema = rest ? tupleSchema.rest(rest) : tupleSchema; + // Apply minItems/maxItems constraints to tuples + if (typeof schema.minItems === "number") { + zodSchema = zodSchema.check(z.minLength(schema.minItems)); + } + if (typeof schema.maxItems === "number") { + zodSchema = zodSchema.check(z.maxLength(schema.maxItems)); + } + } + else if (Array.isArray(items)) { + // Tuple with items array (draft-7) + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0; + const tupleItems = items.map((item) => convertSchema(item, ctx)); + const positionalItems = applyMinItems(tupleItems, minItems); + const rest = getTupleRest(schema.additionalItems, ctx); + const tupleSchema = z.tuple(positionalItems); + zodSchema = rest ? tupleSchema.rest(rest) : tupleSchema; + // Apply minItems/maxItems constraints to tuples + if (typeof schema.minItems === "number") { + zodSchema = zodSchema.check(z.minLength(schema.minItems)); + } + if (typeof schema.maxItems === "number") { + zodSchema = zodSchema.check(z.maxLength(schema.maxItems)); + } + } + else if (items !== undefined) { + // Regular array + const element = convertSchema(items, ctx); + let arraySchema = z.array(element); + // Apply constraints + if (typeof schema.minItems === "number") { + arraySchema = arraySchema.min(schema.minItems); + } + if (typeof schema.maxItems === "number") { + arraySchema = arraySchema.max(schema.maxItems); + } + zodSchema = arraySchema; + } + else { + // No items specified - array of any + zodSchema = z.array(z.any()); + } + // minContains/maxContains only constrain anything when `contains` itself is present + if (schema.uniqueItems === true || schema.contains !== undefined) { + zodSchema = checkArrayGuards(zodSchema, { + uniqueItems: schema.uniqueItems === true, + containsSchema: schema.contains !== undefined ? convertSchema(schema.contains, ctx) : undefined, + minContains: typeof schema.minContains === "number" ? schema.minContains : undefined, + maxContains: typeof schema.maxContains === "number" ? schema.maxContains : undefined, + }); + } + break; + } + default: + throw new Error(`Unsupported type: ${type}`); + } + return zodSchema; +} +function convertSchema(schema, ctx) { + if (typeof schema === "boolean") { + return schema ? z.any() : z.never(); + } + // Convert base schema first (ignoring composition keywords) + let baseSchema = convertBaseSchema(schema, ctx); + const hasExplicitType = schema.type || schema.enum !== undefined || schema.const !== undefined; + // Process composition keywords LAST (they can appear together) + // Handle anyOf - wrap base schema with union + if (schema.anyOf && Array.isArray(schema.anyOf)) { + const options = schema.anyOf.map((s) => convertSchema(s, ctx)); + const anyOfUnion = z.union(options); + baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion; + } + // Handle oneOf - exclusive union (exactly one must match) + if (schema.oneOf && Array.isArray(schema.oneOf)) { + const options = schema.oneOf.map((s) => convertSchema(s, ctx)); + const oneOfUnion = z.xor(options); + baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion; + } + // Handle allOf - wrap base schema with intersection + if (schema.allOf && Array.isArray(schema.allOf)) { + if (schema.allOf.length === 0) { + baseSchema = hasExplicitType ? baseSchema : z.any(); + } + else { + let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx); + const startIdx = hasExplicitType ? 0 : 1; + for (let i = startIdx; i < schema.allOf.length; i++) { + result = z.intersection(result, convertSchema(schema.allOf[i], ctx)); + } + baseSchema = result; + } + } + // Handle nullable (OpenAPI 3.0) + if (schema.nullable === true && ctx.version === "openapi-3.0") { + baseSchema = z.nullable(baseSchema); + } + // Handle readOnly + if (schema.readOnly === true) { + baseSchema = z.readonly(baseSchema); + } + // Apply `default` so it wraps the fully-composed schema. This ensures `parse(undefined) -> default` works regardless of which branch of `convertBaseSchema` produced the inner schema (enum/const/not/typed/etc.). + if (schema.default !== undefined) { + baseSchema = baseSchema.default(schema.default); + } + // Collect non-description annotation metadata into the user-supplied registry. Description is handled separately below via `.describe()` to preserve the contract that `schema.description` reads from globalRegistry. + const extraMeta = {}; + const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"]; + for (const key of coreMetadataKeys) { + if (key in schema) { + extraMeta[key] = schema[key]; + } + } + const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"]; + for (const key of contentMetadataKeys) { + if (key in schema) { + extraMeta[key] = schema[key]; + } + } + // `propertyNames` and `contains` are enforced by a guard, which `toJSONSchema` cannot infer, so the original keyword is carried as metadata to keep the round-trip lossless. Only where it was actually applied: on any other type it is inert, and on a `$ref` the metadata would land on the target every reference shares. A carried subschema is copied verbatim while the root `$defs` stay behind, so one holding a `$ref` is dropped rather than emitted as a dangling reference. + if (schema.type === "object" && schema.$ref === undefined) { + if (schema.propertyNames !== undefined && !containsRef(schema.propertyNames)) { + extraMeta.propertyNames = schema.propertyNames; + } + for (const key of ["minProperties", "maxProperties"]) { + if (schema[key] !== undefined) + extraMeta[key] = schema[key]; + } + } + if (schema.type === "array" && schema.$ref === undefined) { + if (schema.contains !== undefined && !containsRef(schema.contains)) { + extraMeta.contains = schema.contains; + } + for (const key of ["uniqueItems", "minContains", "maxContains"]) { + if (schema[key] !== undefined) + extraMeta[key] = schema[key]; + } + } + for (const key of Object.keys(schema)) { + if (!RECOGNIZED_KEYS.has(key)) { + assignProp(extraMeta, key, schema[key]); + } + } + if (Object.keys(extraMeta).length > 0) { + ctx.registry.add(baseSchema, extraMeta); + } + // Apply description last. `.describe()` clones the schema and sets `_zod.parent` on the clone, so registry lookups on the returned reference still resolve `extraMeta` via parent inheritance. + if (schema.description) { + baseSchema = baseSchema.describe(schema.description); + } + return baseSchema; +} +/** + * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */ +export function fromJSONSchema(schema, params) { + // Handle boolean schemas + if (typeof schema === "boolean") { + return schema ? z.any() : z.never(); + } + // Normalize input via a JSON round-trip. This guarantees the converter walks a plain, finite, JSON-valid object graph: cyclic inputs fail here, getter/Proxy-based properties are materialized into static values, and class instances collapse to plain objects. + let normalized; + try { + normalized = JSON.parse(JSON.stringify(schema)); + } + catch { + throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas"); + } + const version = detectVersion(normalized, params?.defaultTarget); + const defs = (normalized.$defs || normalized.definitions || {}); + const ctx = { + version, + defs, + refs: new Map(), + processing: new Set(), + rootSchema: normalized, + registry: params?.registry ?? globalRegistry, + }; + return convertSchema(normalized, ctx); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts new file mode 100644 index 000000000..09b530fe2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts @@ -0,0 +1,8 @@ +import type * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +export type input = core.input; +export type output = core.output; +/** Returns a copy of the schema with every pipe replaced by its input side. A codec's checks are dropped: they constrain the decoded value the input side never produces. */ +export declare function input(schema: T): schemas.ZodType, core.input>; +/** Returns a copy of the schema with every pipe replaced by its output side, carrying over the pipe's own checks. */ +export declare function output(schema: T): schemas.ZodType, core.output>; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js new file mode 100644 index 000000000..4a0002688 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js @@ -0,0 +1,36 @@ +import { clone, mergeDefs } from "../core/util.js"; +import { visit } from "../core/visit.js"; +import * as schemas from "./schemas.js"; +/** Appends a pipe's own checks to the side that replaces it, mirroring how `.check()` clones. */ +function withChecks(side, checks) { + if (!checks?.length) + return side; + const def = side._zod.def; + return clone(side, mergeDefs(def, { checks: [...(def.checks ?? []), ...checks] }), { parent: true }); +} +/** The out side, carrying the pipe's own checks: those run against the decoded value, which is what `out` produces. */ +function outSide(def) { + return withChecks(def.out, def.checks); +} +/** The in side. `z.preprocess` pipes a transform into a schema, and a bare transform validates nothing, so the schema it feeds is the real input side — the resolution `toJSONSchema` already makes for this case. */ +function inSide(def) { + return def.in._zod.traits.has("$ZodTransform") ? outSide(def) : def.in; +} +/** Returns a copy of the schema with every pipe replaced by its input side. A codec's checks are dropped: they constrain the decoded value the input side never produces. */ +export function input(schema) { + return visit(schema, { + pipe: (s) => inSide(s._zod.def), + // A default value belongs to the output side, so a rewritten inner type leaves it stranded. `.default()` widens the declared input type with `undefined`, and `optional` is what carries that across. + default: (s, rewritten) => (rewritten ? schemas.optional(s._zod.def.innerType) : s), + // A catch value is output-side too, but `.catch()` leaves the declared input type alone, so the inner schema stands on its own. + catch: (s, rewritten) => (rewritten ? s._zod.def.innerType : s), + }); +} +/** Returns a copy of the schema with every pipe replaced by its output side, carrying over the pipe's own checks. */ +export function output(schema) { + return visit(schema, { + pipe: (s) => outSide(s._zod.def), + // A prefault value is fed through the schema, which makes it input-side, so a rewritten inner type leaves it stranded. + prefault: (s, rewritten) => (rewritten ? s._zod.def.innerType : s), + }); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts new file mode 100644 index 000000000..b5fc2a17b --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts @@ -0,0 +1,3 @@ +import * as z from "./external.cjs"; +export * from "./external.cjs"; +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts new file mode 100644 index 000000000..db764d9f8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts @@ -0,0 +1,7 @@ +import * as core from "../core/index.cjs"; +import { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.cjs"; +export { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.cjs"; +export declare function datetime(params?: string | core.$ZodISODateTimeParams): ZodISODateTime; +export declare function date(params?: string | core.$ZodISODateParams): ZodISODate; +export declare function time(params?: string | core.$ZodISOTimeParams): ZodISOTime; +export declare function duration(params?: string | core.$ZodISODurationParams): ZodISODuration; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js new file mode 100644 index 000000000..d38f24ddd --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js @@ -0,0 +1,15 @@ +import * as core from "../core/index.js"; +import { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.js"; +export { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.js"; +export function datetime(params) { + return core._isoDateTime(ZodISODateTime, params); +} +export function date(params) { + return core._isoDate(ZodISODate, params); +} +export function time(params) { + return core._isoTime(ZodISOTime, params); +} +export function duration(params) { + return core._isoDuration(ZodISODuration, params); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts new file mode 100644 index 000000000..408566b52 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts @@ -0,0 +1,44 @@ +import * as core from "../core/index.cjs"; +import { type ZodError } from "./errors.cjs"; +export type ZodSafeParseResult = ZodSafeParseSuccess | ZodSafeParseError; +export type ZodSafeParseSuccess = { + success: true; + data: T; + error?: never; +}; +export type ZodSafeParseError = { + success: false; + data?: never; + error: ZodError; +}; +export declare const parse: (schema: T, value: unknown, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.output; +export declare const parseAsync: (schema: T, value: unknown, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const safeParse: (schema: T, value: unknown, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeParseAsync: (schema: T, value: unknown, _ctx?: core.ParseContext) => Promise>>; +export { validate, validateAsync } from "../core/index.cjs"; +export declare const encode: (schema: T, value: core.output, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.input; +export declare const decode: (schema: T, value: core.input, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.output; +export declare const encodeAsync: (schema: T, value: core.output, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const decodeAsync: (schema: T, value: core.input, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const safeEncode: (schema: T, value: core.output, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeDecode: (schema: T, value: core.input, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeEncodeAsync: (schema: T, value: core.output, _ctx?: core.ParseContext) => Promise>>; +export declare const safeDecodeAsync: (schema: T, value: core.input, _ctx?: core.ParseContext) => Promise>>; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js new file mode 100644 index 000000000..514aacc64 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js @@ -0,0 +1,16 @@ +import * as core from "../core/index.js"; +import { ZodRealError } from "./errors.js"; +export const parse = /* @__PURE__ */ core._parse(ZodRealError); +export const parseAsync = /* @__PURE__ */ core._parseAsync(ZodRealError); +export const safeParse = /* @__PURE__ */ core._safeParse(ZodRealError); +export const safeParseAsync = /* @__PURE__ */ core._safeParseAsync(ZodRealError); +export { validate, validateAsync } from "../core/index.js"; +// Codec functions +export const encode = /* @__PURE__ */ core._encode(ZodRealError); +export const decode = /* @__PURE__ */ core._decode(ZodRealError); +export const encodeAsync = /* @__PURE__ */ core._encodeAsync(ZodRealError); +export const decodeAsync = /* @__PURE__ */ core._decodeAsync(ZodRealError); +export const safeEncode = /* @__PURE__ */ core._safeEncode(ZodRealError); +export const safeDecode = /* @__PURE__ */ core._safeDecode(ZodRealError); +export const safeEncodeAsync = /* @__PURE__ */ core._safeEncodeAsync(ZodRealError); +export const safeDecodeAsync = /* @__PURE__ */ core._safeDecodeAsync(ZodRealError); diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts new file mode 100644 index 000000000..226a3ae25 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts @@ -0,0 +1,816 @@ +import type { $ZodBigIntFormats } from "../core/checks.cjs"; +import * as core from "../core/index.cjs"; +import { util, type $ZodNumberFormats } from "../core/index.cjs"; +import type { StandardSchemaWithJSONProps } from "../core/standard-schema.cjs"; +import * as parse from "./parse.cjs"; +export type ZodStandardSchemaWithJSON = StandardSchemaWithJSONProps, core.output>; +export interface ZodType = core.$ZodTypeInternals> extends core.$ZodType { + def: Internals["def"]; + type: Internals["def"]["type"]; + /** @deprecated Use `.def` instead. */ + _def: Internals["def"]; + /** @deprecated Use `z.output` instead. */ + _output: Internals["output"]; + /** @deprecated Use `z.input` instead. */ + _input: Internals["input"]; + "~standard": ZodStandardSchemaWithJSON; + /** Converts this schema to a JSON Schema representation. */ + toJSONSchema(params?: core.ToJSONSchemaParams): core.ZodStandardJSONSchemaPayload; + check(...checks: (core.CheckFn> | core.$ZodCheck>)[]): this; + with(...checks: (core.CheckFn> | core.$ZodCheck>)[]): this; + clone(def?: Internals["def"], params?: { + parent: boolean; + }): this; + register(registry: R, ...meta: this extends R["_schema"] ? undefined extends R["_meta"] ? [core.$replace?] : [core.$replace] : ["Incompatible schema"]): this; + brand(value?: T): PropertyKey extends T ? this : core.$ZodBranded; + parse(data: unknown, params?: core.ParseContext): core.output; + safeParse(data: unknown, params?: core.ParseContext): parse.ZodSafeParseResult>; + parseAsync(data: unknown, params?: core.ParseContext): Promise>; + safeParseAsync(data: unknown, params?: core.ParseContext): Promise>>; + spa: (data: unknown, params?: core.ParseContext) => Promise>>; + validate(data: unknown, params?: core.ParseContext): data is core.input; + validateAsync(data: unknown, params?: core.ParseContext): Promise; + encode(data: core.output, params?: core.ParseContext): core.input; + decode(data: core.input, params?: core.ParseContext): core.output; + encodeAsync(data: core.output, params?: core.ParseContext): Promise>; + decodeAsync(data: core.input, params?: core.ParseContext): Promise>; + safeEncode(data: core.output, params?: core.ParseContext): parse.ZodSafeParseResult>; + safeDecode(data: core.input, params?: core.ParseContext): parse.ZodSafeParseResult>; + safeEncodeAsync(data: core.output, params?: core.ParseContext): Promise>>; + safeDecodeAsync(data: core.input, params?: core.ParseContext): Promise>>; + refine) => unknown | Promise>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? this & ZodType> : this; + superRefine(refinement: (arg: core.output, ctx: core.$RefinementCtx>) => void | Promise, params?: core.$ZodSuperRefineParams): this; + overwrite(fn: (x: core.output) => core.output): this; + optional(): ZodOptional; + exactOptional(): ZodExactOptional; + nonoptional(params?: string | core.$ZodNonOptionalParams): ZodNonOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + default(def: util.NoUndefined>): ZodDefault; + default(def: () => util.NoUndefined>): ZodDefault; + prefault(def: () => core.input): ZodPrefault; + prefault(def: core.input): ZodPrefault; + array(): ZodArray; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: core.output, ctx: core.$RefinementCtx>) => NewOut | Promise): ZodPipe, core.output>>; + catch(def: core.output): ZodCatch; + catch(def: (ctx: core.$ZodCatchCtx) => core.output): ZodCatch; + pipe>>(target: T | core.$ZodType>): ZodPipe; + readonly(): ZodReadonly; + /** Returns a new instance that has been registered in `z.globalRegistry` with the specified description */ + describe(description: string): this; + description?: string; + /** Returns the metadata associated with this instance in `z.globalRegistry` */ + meta(): core.$replace | undefined; + /** Returns a new instance that has been registered in `z.globalRegistry` with the specified metadata */ + meta(data: core.$replace): this; + /** @deprecated Try safe-parsing `undefined` (this is what `isOptional` does internally): + * + * ```ts + * const schema = z.string().optional(); + * const isOptional = schema.safeParse(undefined).success; // true + * ``` + */ + isOptional(): boolean; + /** + * @deprecated Try safe-parsing `null` (this is what `isNullable` does internally): + * + * ```ts + * const schema = z.string().nullable(); + * const isNullable = schema.safeParse(null).success; // true + * ``` + */ + isNullable(): boolean; + apply(fn: (schema: this, ...args: TArgs) => T, ...args: TArgs): T; +} +export interface _ZodType extends ZodType { +} +export declare const ZodType: core.$constructor; +export interface _ZodString = core.$ZodStringInternals> extends _ZodType { + format: string | null; + minLength: number | null; + maxLength: number | null; + regex(regex: RegExp, params?: string | core.$ZodCheckRegexParams): this; + includes(value: string, params?: string | core.$ZodCheckIncludesParams): this; + startsWith(value: string, params?: string | core.$ZodCheckStartsWithParams): this; + endsWith(value: string, params?: string | core.$ZodCheckEndsWithParams): this; + min(minLength: number, params?: string | core.$ZodCheckMinLengthParams): this; + max(maxLength: number, params?: string | core.$ZodCheckMaxLengthParams): this; + length(len: number, params?: string | core.$ZodCheckLengthEqualsParams): this; + nonempty(params?: string | core.$ZodCheckMinLengthParams): this; + lowercase(params?: string | core.$ZodCheckLowerCaseParams): this; + uppercase(params?: string | core.$ZodCheckUpperCaseParams): this; + trim(): this; + normalize(form?: "NFC" | "NFD" | "NFKC" | "NFKD" | (string & {})): this; + toLowerCase(): this; + toUpperCase(): this; + slugify(): this; +} +/** @internal */ +export declare const _ZodString: core.$constructor<_ZodString>; +export interface ZodString extends _ZodString> { + /** @deprecated Use `z.email()` instead. */ + email(params?: string | core.$ZodCheckEmailParams): this; + /** @deprecated Use `z.url()` instead. */ + url(params?: string | core.$ZodCheckURLParams): this; + /** @deprecated Use `z.jwt()` instead. */ + jwt(params?: string | core.$ZodCheckJWTParams): this; + /** @deprecated Use `z.emoji()` instead. */ + emoji(params?: string | core.$ZodCheckEmojiParams): this; + /** @deprecated Use `z.guid()` instead. */ + guid(params?: string | core.$ZodCheckGUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuid(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv4(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv6(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv7(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.nanoid()` instead. */ + nanoid(params?: string | core.$ZodCheckNanoIDParams): this; + /** @deprecated Use `z.guid()` instead. */ + guid(params?: string | core.$ZodCheckGUIDParams): this; + /** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use `z.cuid2()` instead. + * See https://github.com/paralleldrive/cuid. + */ + cuid(params?: string | core.$ZodCheckCUIDParams): this; + /** @deprecated Use `z.cuid2()` instead. */ + cuid2(params?: string | core.$ZodCheckCUID2Params): this; + /** @deprecated Use `z.ulid()` instead. */ + ulid(params?: string | core.$ZodCheckULIDParams): this; + /** @deprecated Use `z.base64()` instead. */ + base64(params?: string | core.$ZodCheckBase64Params): this; + /** @deprecated Use `z.base64url()` instead. */ + base64url(params?: string | core.$ZodCheckBase64URLParams): this; + /** @deprecated Use `z.xid()` instead. */ + xid(params?: string | core.$ZodCheckXIDParams): this; + /** @deprecated Use `z.ksuid()` instead. */ + ksuid(params?: string | core.$ZodCheckKSUIDParams): this; + /** @deprecated Use `z.ipv4()` instead. */ + ipv4(params?: string | core.$ZodCheckIPv4Params): this; + /** @deprecated Use `z.ipv6()` instead. */ + ipv6(params?: string | core.$ZodCheckIPv6Params): this; + /** @deprecated Use `z.cidrv4()` instead. */ + cidrv4(params?: string | core.$ZodCheckCIDRv4Params): this; + /** @deprecated Use `z.cidrv6()` instead. */ + cidrv6(params?: string | core.$ZodCheckCIDRv6Params): this; + /** @deprecated Use `z.e164()` instead. */ + e164(params?: string | core.$ZodCheckE164Params): this; + /** @deprecated Use `z.iso.datetime()` instead. */ + datetime(params?: string | core.$ZodCheckISODateTimeParams): this; + /** @deprecated Use `z.iso.date()` instead. */ + date(params?: string | core.$ZodCheckISODateParams): this; + /** @deprecated Use `z.iso.time()` instead. */ + time(params?: string | core.$ZodCheckISOTimeParams): this; + /** @deprecated Use `z.iso.duration()` instead. */ + duration(params?: string | core.$ZodCheckISODurationParams): this; +} +export declare const ZodString: core.$constructor; +export declare function string(params?: string | core.$ZodStringParams): ZodString; +export declare function string(params?: string | core.$ZodStringParams): core.$ZodType; +export interface ZodStringFormat extends _ZodString> { +} +export declare const ZodStringFormat: core.$constructor; +export interface ZodISODateTime extends ZodStringFormat { + _zod: core.$ZodISODateTimeInternals; +} +export declare const ZodISODateTime: core.$constructor; +export interface ZodISODate extends ZodStringFormat { + _zod: core.$ZodISODateInternals; +} +export declare const ZodISODate: core.$constructor; +export interface ZodISOTime extends ZodStringFormat { + _zod: core.$ZodISOTimeInternals; +} +export declare const ZodISOTime: core.$constructor; +export interface ZodISODuration extends ZodStringFormat { + _zod: core.$ZodISODurationInternals; +} +export declare const ZodISODuration: core.$constructor; +export interface ZodEmail extends ZodStringFormat<"email"> { + _zod: core.$ZodEmailInternals; +} +export declare const ZodEmail: core.$constructor; +export declare function email(params?: string | core.$ZodEmailParams): ZodEmail; +export interface ZodGUID extends ZodStringFormat<"guid"> { + _zod: core.$ZodGUIDInternals; +} +export declare const ZodGUID: core.$constructor; +export declare function guid(params?: string | core.$ZodGUIDParams): ZodGUID; +export interface ZodUUID extends ZodStringFormat<"uuid"> { + _zod: core.$ZodUUIDInternals; +} +export declare const ZodUUID: core.$constructor; +export declare function uuid(params?: string | core.$ZodUUIDParams): ZodUUID; +export declare function uuidv4(params?: string | core.$ZodUUIDv4Params): ZodUUID; +export declare function uuidv6(params?: string | core.$ZodUUIDv6Params): ZodUUID; +export declare function uuidv7(params?: string | core.$ZodUUIDv7Params): ZodUUID; +export interface ZodURL extends ZodStringFormat<"url"> { + _zod: core.$ZodURLInternals; +} +export declare const ZodURL: core.$constructor; +export declare function url(params?: string | core.$ZodURLParams): ZodURL; +export declare function httpUrl(params?: string | Omit): ZodURL; +export interface ZodEmoji extends ZodStringFormat<"emoji"> { + _zod: core.$ZodEmojiInternals; +} +export declare const ZodEmoji: core.$constructor; +export declare function emoji(params?: string | core.$ZodEmojiParams): ZodEmoji; +export interface ZodNanoID extends ZodStringFormat<"nanoid"> { + _zod: core.$ZodNanoIDInternals; +} +export declare const ZodNanoID: core.$constructor; +export declare function nanoid(params?: string | core.$ZodNanoIDParams): ZodNanoID; +/** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link ZodCUID2} instead. + * See https://github.com/paralleldrive/cuid. + */ +export interface ZodCUID extends ZodStringFormat<"cuid"> { + _zod: core.$ZodCUIDInternals; +} +/** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link ZodCUID2} instead. + * See https://github.com/paralleldrive/cuid. + */ +export declare const ZodCUID: core.$constructor; +/** + * Validates a CUID v1 string. + * + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link cuid2 | `z.cuid2()`} instead. + * See https://github.com/paralleldrive/cuid. + */ +export declare function cuid(params?: string | core.$ZodCUIDParams): ZodCUID; +export interface ZodCUID2 extends ZodStringFormat<"cuid2"> { + _zod: core.$ZodCUID2Internals; +} +export declare const ZodCUID2: core.$constructor; +export declare function cuid2(params?: string | core.$ZodCUID2Params): ZodCUID2; +export interface ZodULID extends ZodStringFormat<"ulid"> { + _zod: core.$ZodULIDInternals; +} +export declare const ZodULID: core.$constructor; +export declare function ulid(params?: string | core.$ZodULIDParams): ZodULID; +export interface ZodXID extends ZodStringFormat<"xid"> { + _zod: core.$ZodXIDInternals; +} +export declare const ZodXID: core.$constructor; +export declare function xid(params?: string | core.$ZodXIDParams): ZodXID; +export interface ZodKSUID extends ZodStringFormat<"ksuid"> { + _zod: core.$ZodKSUIDInternals; +} +export declare const ZodKSUID: core.$constructor; +export declare function ksuid(params?: string | core.$ZodKSUIDParams): ZodKSUID; +export interface ZodIPv4 extends ZodStringFormat<"ipv4"> { + _zod: core.$ZodIPv4Internals; +} +export declare const ZodIPv4: core.$constructor; +export declare function ipv4(params?: string | core.$ZodIPv4Params): ZodIPv4; +export interface ZodMAC extends ZodStringFormat<"mac"> { + _zod: core.$ZodMACInternals; +} +export declare const ZodMAC: core.$constructor; +export declare function mac(params?: string | core.$ZodMACParams): ZodMAC; +export interface ZodIPv6 extends ZodStringFormat<"ipv6"> { + _zod: core.$ZodIPv6Internals; +} +export declare const ZodIPv6: core.$constructor; +export declare function ipv6(params?: string | core.$ZodIPv6Params): ZodIPv6; +export interface ZodCIDRv4 extends ZodStringFormat<"cidrv4"> { + _zod: core.$ZodCIDRv4Internals; +} +export declare const ZodCIDRv4: core.$constructor; +export declare function cidrv4(params?: string | core.$ZodCIDRv4Params): ZodCIDRv4; +export interface ZodCIDRv6 extends ZodStringFormat<"cidrv6"> { + _zod: core.$ZodCIDRv6Internals; +} +export declare const ZodCIDRv6: core.$constructor; +export declare function cidrv6(params?: string | core.$ZodCIDRv6Params): ZodCIDRv6; +export interface ZodBase64 extends ZodStringFormat<"base64"> { + _zod: core.$ZodBase64Internals; +} +export declare const ZodBase64: core.$constructor; +export declare function base64(params?: string | core.$ZodBase64Params): ZodBase64; +export interface ZodBase64URL extends ZodStringFormat<"base64url"> { + _zod: core.$ZodBase64URLInternals; +} +export declare const ZodBase64URL: core.$constructor; +export declare function base64url(params?: string | core.$ZodBase64URLParams): ZodBase64URL; +export interface ZodE164 extends ZodStringFormat<"e164"> { + _zod: core.$ZodE164Internals; +} +export declare const ZodE164: core.$constructor; +export declare function e164(params?: string | core.$ZodE164Params): ZodE164; +export interface ZodCreditCard extends ZodStringFormat<"credit_card"> { + _zod: core.$ZodCreditCardInternals; +} +export declare const ZodCreditCard: core.$constructor; +export declare function creditCard(params?: string | core.$ZodCreditCardParams): ZodCreditCard; +export interface ZodIBAN extends ZodStringFormat<"iban"> { + _zod: core.$ZodIBANInternals; +} +export declare const ZodIBAN: core.$constructor; +export declare function iban(params?: string | core.$ZodIBANParams): ZodIBAN; +export interface ZodJWT extends ZodStringFormat<"jwt"> { + _zod: core.$ZodJWTInternals; +} +export declare const ZodJWT: core.$constructor; +export declare function jwt(params?: string | core.$ZodJWTParams): ZodJWT; +export interface ZodCustomStringFormat extends ZodStringFormat, core.$ZodCustomStringFormat { + _zod: core.$ZodCustomStringFormatInternals; + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodCustomStringFormat: core.$constructor; +export declare function stringFormat(format: Format, fnOrRegex: ((arg: string) => util.MaybeAsync) | RegExp, _params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat; +export declare function hostname(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hostname">; +export declare function hex(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hex">; +export declare function currencyCode(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"currency_code">; +export declare function hash(alg: Alg, params?: { + enc?: Enc; +} & core.$ZodStringFormatParams): ZodCustomStringFormat<`${Alg}_${Enc}`>; +export interface _ZodNumber extends _ZodType { + gt(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Identical to .min() */ + gte(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + min(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + lt(value: number, params?: string | core.$ZodCheckLessThanParams): this; + /** Identical to .max() */ + lte(value: number, params?: string | core.$ZodCheckLessThanParams): this; + max(value: number, params?: string | core.$ZodCheckLessThanParams): this; + /** Consider `z.int()` instead. This API is considered *legacy*; it will never be removed but a better alternative exists. */ + int(params?: string | core.$ZodCheckNumberFormatParams): this; + /** @deprecated This is now identical to `.int()`. Only numbers in the safe integer range are accepted. */ + safe(params?: string | core.$ZodCheckNumberFormatParams): this; + positive(params?: string | core.$ZodCheckGreaterThanParams): this; + nonnegative(params?: string | core.$ZodCheckGreaterThanParams): this; + negative(params?: string | core.$ZodCheckLessThanParams): this; + nonpositive(params?: string | core.$ZodCheckLessThanParams): this; + multipleOf(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; + /** @deprecated Use `.multipleOf()` instead. */ + step(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; + /** @deprecated In v4 and later, z.number() does not allow infinite values by default. This is a no-op. */ + finite(params?: unknown): this; + minValue: number | null; + maxValue: number | null; + /** @deprecated Check the `format` property instead. */ + isInt: boolean; + /** @deprecated Number schemas no longer accept infinite values, so this always returns `true`. */ + isFinite: boolean; + format: string | null; +} +export interface ZodNumber extends _ZodNumber> { +} +export declare const ZodNumber: core.$constructor; +export declare function number(params?: string | core.$ZodNumberParams): ZodNumber; +export interface ZodNumberFormat extends ZodNumber { + _zod: core.$ZodNumberFormatInternals; +} +export declare const ZodNumberFormat: core.$constructor; +export interface ZodInt extends ZodNumberFormat<"safeint"> { +} +export declare function int(params?: string | core.$ZodCheckNumberFormatParams): ZodInt; +export interface ZodFloat32 extends ZodNumberFormat<"float32"> { +} +export declare function float32(params?: string | core.$ZodCheckNumberFormatParams): ZodFloat32; +export interface ZodFloat64 extends ZodNumberFormat<"float64"> { +} +export declare function float64(params?: string | core.$ZodCheckNumberFormatParams): ZodFloat64; +export interface ZodInt32 extends ZodNumberFormat<"int32"> { +} +export declare function int32(params?: string | core.$ZodCheckNumberFormatParams): ZodInt32; +export interface ZodUInt32 extends ZodNumberFormat<"uint32"> { +} +export declare function uint32(params?: string | core.$ZodCheckNumberFormatParams): ZodUInt32; +export interface _ZodBoolean extends _ZodType { +} +export interface ZodBoolean extends _ZodBoolean> { +} +export declare const ZodBoolean: core.$constructor; +export declare function boolean(params?: string | core.$ZodBooleanParams): ZodBoolean; +export interface _ZodBigInt extends _ZodType { + gte(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Alias of `.gte()` */ + min(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + gt(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Alias of `.lte()` */ + lte(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + max(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + lt(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + positive(params?: string | core.$ZodCheckGreaterThanParams): this; + negative(params?: string | core.$ZodCheckLessThanParams): this; + nonpositive(params?: string | core.$ZodCheckLessThanParams): this; + nonnegative(params?: string | core.$ZodCheckGreaterThanParams): this; + multipleOf(value: bigint, params?: string | core.$ZodCheckMultipleOfParams): this; + minValue: bigint | null; + maxValue: bigint | null; + format: string | null; +} +export interface ZodBigInt extends _ZodBigInt> { +} +export declare const ZodBigInt: core.$constructor; +export declare function bigint(params?: string | core.$ZodBigIntParams): ZodBigInt; +export interface ZodBigIntFormat extends ZodBigInt { + _zod: core.$ZodBigIntFormatInternals; +} +export declare const ZodBigIntFormat: core.$constructor; +export interface ZodInt64 extends ZodBigIntFormat<"int64"> { +} +export declare function int64(params?: string | core.$ZodBigIntFormatParams): ZodInt64; +export interface ZodUInt64 extends ZodBigIntFormat<"uint64"> { +} +export declare function uint64(params?: string | core.$ZodBigIntFormatParams): ZodUInt64; +export interface ZodSymbol extends _ZodType { +} +export declare const ZodSymbol: core.$constructor; +export declare function symbol(params?: string | core.$ZodSymbolParams): ZodSymbol; +export interface ZodUndefined extends _ZodType { +} +export declare const ZodUndefined: core.$constructor; +declare function _undefined(params?: string | core.$ZodUndefinedParams): ZodUndefined; +export { _undefined as undefined }; +export interface ZodNull extends _ZodType { +} +export declare const ZodNull: core.$constructor; +declare function _null(params?: string | core.$ZodNullParams): ZodNull; +export { _null as null }; +export interface ZodAny extends _ZodType { +} +export declare const ZodAny: core.$constructor; +export declare function any(): ZodAny; +export interface ZodUnknown extends _ZodType { +} +export declare const ZodUnknown: core.$constructor; +export declare function unknown(): ZodUnknown; +export interface ZodNever extends _ZodType { +} +export declare const ZodNever: core.$constructor; +export declare function never(params?: string | core.$ZodNeverParams): ZodNever; +export interface ZodVoid extends _ZodType { +} +export declare const ZodVoid: core.$constructor; +declare function _void(params?: string | core.$ZodVoidParams): ZodVoid; +export { _void as void }; +export interface _ZodDate extends _ZodType { + min(value: number | Date, params?: string | core.$ZodCheckGreaterThanParams): this; + max(value: number | Date, params?: string | core.$ZodCheckLessThanParams): this; + /** @deprecated Not recommended. */ + minDate: Date | null; + /** @deprecated Not recommended. */ + maxDate: Date | null; +} +export interface ZodDate extends _ZodDate> { +} +export declare const ZodDate: core.$constructor; +export declare function date(params?: string | core.$ZodDateParams): ZodDate; +export interface ZodArray extends _ZodType>, core.$ZodArray { + element: T; + min(minLength: number, params?: string | core.$ZodCheckMinLengthParams): this; + nonempty(params?: string | core.$ZodCheckMinLengthParams): this; + max(maxLength: number, params?: string | core.$ZodCheckMaxLengthParams): this; + length(len: number, params?: string | core.$ZodCheckLengthEqualsParams): this; + unwrap(): T; + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodArray: core.$constructor; +export declare function array(element: T, params?: string | core.$ZodArrayParams): ZodArray; +export declare function keyof(schema: T): ZodEnum>; +export type SafeExtendShape = { + [K in keyof Ext]: K extends keyof Base ? core.output extends core.output ? core.input extends core.input ? Ext[K] : never : never : Ext[K]; +}; +export interface ZodObject< +/** @ts-ignore Cast variance */ +out Shape extends core.$ZodShape = core.$ZodLooseShape, out Config extends core.$ZodObjectConfig = core.$strip> extends _ZodType>, core.$ZodObject { + "~standard": ZodStandardSchemaWithJSON; + shape: Shape; + keyof(): ZodEnum>; + /** Define a schema to validate all unrecognized keys. This overrides the existing strict/loose behavior. */ + catchall(schema: T): ZodObject>; + /** @deprecated Use `z.looseObject()` or `.loose()` instead. */ + passthrough(): ZodObject; + /** Consider `z.looseObject(A.shape)` instead */ + loose(): ZodObject; + /** Consider `z.strictObject(A.shape)` instead */ + strict(): ZodObject; + /** This is the default behavior. This method call is likely unnecessary. */ + strip(): ZodObject; + extend(shape: U): ZodObject>, Config>; + safeExtend(shape: SafeExtendShape & Partial>): ZodObject>, Config>; + /** + * @deprecated Use [`A.extend(B.shape)`](https://zod.dev/api?id=extend) instead. + */ + merge(other: U): ZodObject, U["_zod"]["config"]>; + pick>(mask: M & Record, never>): ZodObject>>, Config>; + omit>(mask: M & Record, never>): ZodObject>>, Config>; + partial(): ZodObject<{ + -readonly [k in keyof Shape]: ZodOptional; + }, Config>; + partial>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodOptional : Shape[k]; + }, Config>; + exactPartial(): ZodObject<{ + -readonly [k in keyof Shape]: ZodExactOptional; + }, Config>; + exactPartial>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodExactOptional : Shape[k]; + }, Config>; + required(): ZodObject<{ + -readonly [k in keyof Shape]: ZodNonOptional; + }, Config>; + required>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodNonOptional : Shape[k]; + }, Config>; +} +export declare const ZodObject: core.$constructor; +export declare function object>>(shape?: T, params?: string | core.$ZodObjectParams): ZodObject, core.$strip>; +export declare function strictObject(shape: T, params?: string | core.$ZodObjectParams): ZodObject, core.$strict>; +export declare function looseObject(shape: T, params?: string | core.$ZodObjectParams): ZodObject, core.$loose>; +export interface ZodUnion extends _ZodType>, core.$ZodUnion { + "~standard": ZodStandardSchemaWithJSON; + options: T; +} +export declare const ZodUnion: core.$constructor; +export declare function union(options: T, params?: string | core.$ZodUnionParams): ZodUnion; +export interface ZodXor extends _ZodType>, core.$ZodXor { + "~standard": ZodStandardSchemaWithJSON; + options: T; +} +export declare const ZodXor: core.$constructor; +/** Creates an exclusive union (XOR) where exactly one option must match. + * Unlike regular unions that succeed when any option matches, xor fails if + * zero or more than one option matches the input. */ +export declare function xor(options: T, params?: string | core.$ZodXorParams): ZodXor; +export interface ZodDiscriminatedUnion extends ZodUnion, core.$ZodDiscriminatedUnion { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodDiscriminatedUnionInternals; + def: core.$ZodDiscriminatedUnionDef; +} +export declare const ZodDiscriminatedUnion: core.$constructor; +export declare function discriminatedUnion(discriminator: Disc, options: Types, params?: string | core.$ZodDiscriminatedUnionParams): ZodDiscriminatedUnion; +export interface ZodIntersection extends _ZodType>, core.$ZodIntersection { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodIntersection: core.$constructor; +export declare function intersection(left: T, right: U): ZodIntersection; +export interface ZodTuple extends _ZodType>, core.$ZodTuple { + "~standard": ZodStandardSchemaWithJSON; + rest(rest: Rest): ZodTuple; + partial(): ZodTuple<{ + -readonly [k in keyof T]: ZodOptional; + }, Rest>; +} +export declare const ZodTuple: core.$constructor; +export declare function tuple(items: T, params?: string | core.$ZodTupleParams): ZodTuple; +export declare function tuple(items: T, rest: Rest, params?: string | core.$ZodTupleParams): ZodTuple; +export declare function tuple(items: [], params?: string | core.$ZodTupleParams): ZodTuple<[], null>; +export interface ZodRecord extends _ZodType>, core.$ZodRecord { + "~standard": ZodStandardSchemaWithJSON; + keyType: Key; + valueType: Value; +} +export declare const ZodRecord: core.$constructor; +export declare function record(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export declare function partialRecord(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export declare function looseRecord(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export interface ZodMap extends _ZodType>, core.$ZodMap { + "~standard": ZodStandardSchemaWithJSON; + keyType: Key; + valueType: Value; + min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this; + nonempty(params?: string | core.$ZodCheckMinSizeParams): this; + max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this; + size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this; +} +export declare const ZodMap: core.$constructor; +export declare function map(keyType: Key, valueType: Value, params?: string | core.$ZodMapParams): ZodMap; +export interface ZodSet extends _ZodType>, core.$ZodSet { + "~standard": ZodStandardSchemaWithJSON; + min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this; + nonempty(params?: string | core.$ZodCheckMinSizeParams): this; + max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this; + size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this; +} +export declare const ZodSet: core.$constructor; +export declare function set(valueType: Value, params?: string | core.$ZodSetParams): ZodSet; +export interface ZodEnum< +/** @ts-ignore Cast variance */ +out T extends util.EnumLike = util.EnumLike> extends _ZodType>, core.$ZodEnum { + "~standard": ZodStandardSchemaWithJSON; + enum: T; + options: Array; + extract(values: U, params?: string | core.$ZodEnumParams): ZodEnum>>; + exclude(values: U, params?: string | core.$ZodEnumParams): ZodEnum>>; +} +export declare const ZodEnum: core.$constructor; +declare function _enum(values: T, params?: string | core.$ZodEnumParams): ZodEnum>; +declare function _enum(entries: T, params?: string | core.$ZodEnumParams): ZodEnum; +export { _enum as enum }; +/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead. + * + * ```ts + * enum Colors { red, green, blue } + * z.enum(Colors); + * ``` + */ +export declare function nativeEnum(entries: T, params?: string | core.$ZodEnumParams): ZodEnum; +export interface ZodLiteral extends _ZodType>, core.$ZodLiteral { + "~standard": ZodStandardSchemaWithJSON; + values: Set; + /** @legacy Use `.values` instead. Accessing this property will throw an error if the literal accepts multiple values. */ + value: T; +} +export declare const ZodLiteral: core.$constructor; +export declare function literal>(value: T, params?: string | core.$ZodLiteralParams): ZodLiteral; +export declare function literal(value: T, params?: string | core.$ZodLiteralParams): ZodLiteral; +export interface ZodFile extends _ZodType, core.$ZodFile { + "~standard": ZodStandardSchemaWithJSON; + min(size: number, params?: string | core.$ZodCheckMinSizeParams): this; + max(size: number, params?: string | core.$ZodCheckMaxSizeParams): this; + mime(types: util.MimeTypes | Array, params?: string | core.$ZodCheckMimeTypeParams): this; +} +export declare const ZodFile: core.$constructor; +export declare function file(params?: string | core.$ZodFileParams): ZodFile; +export interface ZodTransform extends _ZodType>, core.$ZodTransform { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodTransform: core.$constructor; +export declare function transform(fn: (input: I, ctx: core.$RefinementCtx) => O): ZodTransform, I>; +export interface ZodOptional extends _ZodType>, core.$ZodOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodOptional: core.$constructor; +export declare function optional(innerType: T): ZodOptional; +export interface ZodExactOptional extends _ZodType>, core.$ZodExactOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodExactOptional: core.$constructor; +export declare function exactOptional(innerType: T): ZodExactOptional; +export interface ZodNullable extends _ZodType>, core.$ZodNullable { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodNullable: core.$constructor; +export declare function nullable(innerType: T): ZodNullable; +export declare function nullish(innerType: T): ZodOptional>; +export interface ZodDefault extends _ZodType>, core.$ZodDefault { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; + /** @deprecated Use `.unwrap()` instead. */ + removeDefault(): T; +} +export declare const ZodDefault: core.$constructor; +export declare function _default(innerType: T, defaultValue: util.NoUndefined> | (() => util.NoUndefined>)): ZodDefault; +export interface ZodPrefault extends _ZodType>, core.$ZodPrefault { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodPrefault: core.$constructor; +export declare function prefault(innerType: T, defaultValue: core.input | (() => core.input)): ZodPrefault; +export interface ZodNonOptional extends _ZodType>, core.$ZodNonOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodNonOptional: core.$constructor; +export declare function nonoptional(innerType: T, params?: string | core.$ZodNonOptionalParams): ZodNonOptional; +export interface ZodSuccess extends _ZodType>, core.$ZodSuccess { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodSuccess: core.$constructor; +export declare function success(innerType: T): ZodSuccess; +export interface ZodCatch extends _ZodType>, core.$ZodCatch { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; + /** @deprecated Use `.unwrap()` instead. */ + removeCatch(): T; +} +export declare const ZodCatch: core.$constructor; +declare function _catch(innerType: T, catchValue: core.output | ((ctx: core.$ZodCatchCtx) => core.output)): ZodCatch; +export { _catch as catch }; +export interface ZodNaN extends _ZodType, core.$ZodNaN { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodNaN: core.$constructor; +export declare function nan(params?: string | core.$ZodNaNParams): ZodNaN; +export interface ZodPipe extends _ZodType>, core.$ZodPipe { + "~standard": ZodStandardSchemaWithJSON; + in: A; + out: B; +} +export declare const ZodPipe: core.$constructor; +export declare function pipe> = core.$ZodType>>(in_: A, out: B | core.$ZodType>): ZodPipe; +export interface ZodCodec extends ZodPipe, core.$ZodCodec { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodCodecInternals; + def: core.$ZodCodecDef; +} +export declare const ZodCodec: core.$constructor; +export declare function codec(in_: A, out: B, params: { + decode: (value: core.output, payload: core.ParsePayload>) => core.util.MaybeAsync>; + encode: (value: core.input, payload: core.ParsePayload>) => core.util.MaybeAsync>; +}): ZodCodec; +export declare function invertCodec(codec: ZodCodec): ZodCodec; +export interface ZodPreprocess extends ZodPipe, B>, core.$ZodPreprocess { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodPreprocessInternals; + def: core.$ZodPreprocessDef; +} +export declare const ZodPreprocess: core.$constructor; +export interface ZodReadonly extends _ZodType>, core.$ZodReadonly { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodReadonly: core.$constructor; +export declare function readonly(innerType: T): ZodReadonly; +export interface ZodTemplateLiteral