From f1d694b47b6169af290d158e9b25d709e2b0cd49 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 18:58:42 -0500 Subject: [PATCH 001/169] Move the emitter's loop optimization state into loop-state.ts LlEmitter's field list and constructor are where most concurrent edits to emitter.ts collide. A feature now defines its state in its own file with defineModuleState (emitter-state.ts) and reads it through the returned key, so it no longer edits the class. The loop optimizations' twelve fields and their per-function setup move to loop-state.ts; the emitted IR is unchanged. scripts/split holds the transforms used for the emitter and lowering splits, and rehome-split.mjs, which carries an open branch across a split. --- .../src/backend/llvm/emit-function.ts | 24 +- .../src/backend/llvm/emit-statements.ts | 55 +- .../src/backend/llvm/emitter-state.ts | 24 + packages/compiler/src/backend/llvm/emitter.ts | 34 +- .../compiler/src/backend/llvm/loop-state.ts | 76 +++ scripts/split/README.md | 22 + .../configs/loop-state/emitter-state.template | 24 + .../configs/loop-state/loop-state.config.json | 29 + .../configs/loop-state/loop-state.template | 34 ++ scripts/split/move-state.mjs | 202 +++++++ scripts/split/rehome-split.mjs | 311 ++++++++++ scripts/split/split-methods.mjs | 414 ++++++++++++++ scripts/split/split-module.mjs | 539 ++++++++++++++++++ 13 files changed, 1714 insertions(+), 74 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/emitter-state.ts create mode 100644 packages/compiler/src/backend/llvm/loop-state.ts create mode 100644 scripts/split/README.md create mode 100644 scripts/split/configs/loop-state/emitter-state.template create mode 100644 scripts/split/configs/loop-state/loop-state.config.json create mode 100644 scripts/split/configs/loop-state/loop-state.template create mode 100644 scripts/split/move-state.mjs create mode 100644 scripts/split/rehome-split.mjs create mode 100644 scripts/split/split-methods.mjs create mode 100644 scripts/split/split-module.mjs diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 9e50bea6d..ace696061 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -16,9 +16,7 @@ import { withInitializerBindings, withInitializerLocals, } from "../../ir/initializer-bindings.js"; -import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; import { constructorClass } from "../../ir/final-fields.js"; -import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { findLoopArrayBorrows } from "./loop-array-borrows.js"; import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js"; import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js"; @@ -54,6 +52,7 @@ import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js"; import { f64Lit } from "./common.js"; import { RESULT_ENTRY_PREFIX } from "./record-results.js"; import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; +import { beginFunctionLoops, LOOPS } from "./loop-state.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); @@ -95,7 +94,6 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const initializerBindings = emitter.initializerBindings.get(fn.name) ?? []; const numericFn = withInitializerBindings(fn, initializerBindings); emitter.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l])); - emitter.unaliasedArrays = findUnaliasedLocalArrays(fn); emitter.borrowedParameters.clear(); emitter.referenceWrites?.enter(fn); emitter.reboundOwnerSlots.clear(); @@ -126,8 +124,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.stableCaptureIds = fn.captures !== undefined ? emitter.lazyCaptures.stableCaptures(fn.name) : new Set(); emitter.integerLoopBindings.clear(); - emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; - emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled; + beginFunctionLoops(emitter, fn, numericFn); + emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled; emitter.speculativeFunction = fn.name; emitter.speculation = null; emitter.speculationResume = null; @@ -136,20 +134,6 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { !fn.generator && constructorClass(fn.name) === null && emitter.finalFields.size > 0; - emitter.byteWindowsEnabled = emitter.countedLoopsEnabled; - emitter.bytesLoopVersionsEnabled = emitter.countedLoopsEnabled; - emitter.bytesLoopEntries = null; - emitter.bytesLoopFunction = fn; - emitter.denseLoopsEnabled = emitter.countedLoopsEnabled; - emitter.denseLoop = null; - emitter.denseLoopEntries = - emitter.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") - ? findInitializedByteLoopBindings(fn) - : new Map(); - emitter.byteWindowEntries = - emitter.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") - ? findInitializedByteLoopBindings(fn) - : new Map(); emitter.integerViews.clear(); emitter.fieldPointerTags.clear(); emitter.integerArrayBindings.clear(); @@ -403,7 +387,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { if (local.boxed || emitter.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`); B.volatileSlots = slots; // Without optimization a versioned copy only adds code. - emitter.bytesLoopVersionsEnabled = false; + LOOPS.of(emitter).bytesLoopVersionsEnabled = false; } // Captured bindings come in through the environment — borrowed for the // whole call (the closure owns them): bound here, never released here. diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index da2ffbcab..e00abb8d1 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -67,6 +67,7 @@ import { stringParts, } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; +import { LOOPS } from "./loop-state.js"; export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const B = emitter.B; @@ -855,26 +856,26 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { integerArrayLoop ?? (emitter.debug === null ? matchIntegerBytesForLoop(s, emitter.numericLocals) : null); const countedLoop = - !integerLoop && emitter.countedLoopsEnabled + !integerLoop && LOOPS.of(emitter).countedLoopsEnabled ? matchIntegerCountedForLoop( s, emitter.numericLocals, emitter.integerRanges, - emitter.unaliasedArrays, + LOOPS.of(emitter).unaliasedArrays, ) : null; const window = - countedLoop && emitter.byteWindowsEnabled + countedLoop && LOOPS.of(emitter).byteWindowsEnabled ? matchByteWindow( s, countedLoop, emitter.numericLocals, emitter.captureIds, emitter.integerRanges, - emitter.byteWindowEntries.get(s) ?? new Set(), + LOOPS.of(emitter).byteWindowEntries.get(s) ?? new Set(), ) : null; - const denseShape = !emitter.denseLoopsEnabled + const denseShape = !LOOPS.of(emitter).denseLoopsEnabled ? null : integerArrayLoop ? { @@ -891,8 +892,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s, denseShape, emitter.numericLocals, - emitter.unaliasedArrays, - emitter.denseLoopEntries.get(s) ?? new Set(), + LOOPS.of(emitter).unaliasedArrays, + LOOPS.of(emitter).denseLoopEntries.get(s) ?? new Set(), ) : null; if (dense) { @@ -901,18 +902,18 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { slow = B.newLabel("dense.loop.slow"), done = B.newLabel("dense.loop.done"); B.condBr(valid, fast, slow); - emitter.denseLoopsEnabled = false; + LOOPS.of(emitter).denseLoopsEnabled = false; try { B.startBlock(slow); emitter.emitStmt(s); B.br(done); B.startBlock(fast); - emitter.denseLoop = dense; + LOOPS.of(emitter).denseLoop = dense; emitter.emitStmt(s); B.br(done); } finally { - emitter.denseLoop = null; - emitter.denseLoopsEnabled = true; + LOOPS.of(emitter).denseLoop = null; + LOOPS.of(emitter).denseLoopsEnabled = true; } B.startBlock(done); break; @@ -925,7 +926,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(valid, fast, slow); const originalRanges = emitter.integerRanges, originalBounds = emitter.bytesBounds; - emitter.byteWindowsEnabled = false; + LOOPS.of(emitter).byteWindowsEnabled = false; try { B.startBlock(slow); emitter.emitStmt(s); @@ -938,15 +939,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { } finally { emitter.integerRanges = originalRanges; emitter.bytesBounds = originalBounds; - emitter.byteWindowsEnabled = true; + LOOPS.of(emitter).byteWindowsEnabled = true; } B.startBlock(done); break; } const version = - emitter.bytesLoopVersionsEnabled && - emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && - !emitter.bytesLoopVersioning.has(s) + LOOPS.of(emitter).bytesLoopVersionsEnabled && + LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !LOOPS.of(emitter).bytesLoopVersioning.has(s) ? emitter.matchBytesLoopVersion(s, countedLoop) : null; if (version) { @@ -956,7 +957,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitBytesLoopVersionGuard(emitter, version, fast, slow); const originalBounds = emitter.bytesBounds, originalRanges = emitter.integerRanges; - emitter.bytesLoopVersioning.add(s); + LOOPS.of(emitter).bytesLoopVersioning.add(s); try { B.startBlock(slow); emitter.emitStmt(s); @@ -965,29 +966,29 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) emitter.provenCountedGuards.add(s); + if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); emitter.emitStmt(s); B.br(done); } finally { emitter.bytesBounds = originalBounds; emitter.integerRanges = originalRanges; - emitter.provenCountedGuards.delete(s); - emitter.bytesLoopVersioning.delete(s); + LOOPS.of(emitter).provenCountedGuards.delete(s); + LOOPS.of(emitter).bytesLoopVersioning.delete(s); } B.startBlock(done); break; } let countedJoin: string | null = null; - if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) { + if (countedLoop?.guarded && !LOOPS.of(emitter).provenCountedGuards.has(s)) { const safe = emitCountedLoopSafety(emitter, countedLoop); const fast = B.newLabel("counted.fast"); const slow = B.newLabel("counted.slow"); countedJoin = B.newLabel("counted.done"); B.condBr(safe, fast, slow); B.startBlock(slow); - emitter.countedLoopsEnabled = false; + LOOPS.of(emitter).countedLoopsEnabled = false; emitter.emitStmt(s); - emitter.countedLoopsEnabled = true; + LOOPS.of(emitter).countedLoopsEnabled = true; B.br(countedJoin); B.startBlock(fast); } @@ -1015,8 +1016,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { slot: integerSlot, type: emitter.sizeType, range: - emitter.denseLoop?.counter === integerLoop.localId - ? emitter.denseLoop.range + LOOPS.of(emitter).denseLoop?.counter === integerLoop.localId + ? LOOPS.of(emitter).denseLoop!.range : { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 }, }); } else if (countedLoop) { @@ -1033,8 +1034,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { type: "i64", signed: true, range: - emitter.denseLoop?.counter === countedLoop.localId - ? emitter.denseLoop.range + LOOPS.of(emitter).denseLoop?.counter === countedLoop.localId + ? LOOPS.of(emitter).denseLoop!.range : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); diff --git a/packages/compiler/src/backend/llvm/emitter-state.ts b/packages/compiler/src/backend/llvm/emitter-state.ts new file mode 100644 index 000000000..62dc47b4e --- /dev/null +++ b/packages/compiler/src/backend/llvm/emitter-state.ts @@ -0,0 +1,24 @@ +/* Per-feature emitter state. A feature defines its state in its own file + * with defineModuleState and reads it through the returned key, so adding + * or extending a feature does not edit LlEmitter's field list or + * constructor. */ +import type { LlEmitter } from "./emitter.js"; + +/** A feature's state on an emitter, read with `key.of(emitter)`. */ +export interface EmitterStateKey { + of(emitter: LlEmitter): T; +} + +let moduleSlots = 0; + +/** State that lives for the whole module, created on first read. */ +export function defineModuleState(create: (emitter: LlEmitter) => T): EmitterStateKey { + const slot = moduleSlots++; + return { + of(emitter) { + const states = emitter.moduleStates; + if (slot >= states.length || states[slot] === undefined) states[slot] = create(emitter); + return states[slot] as T; + }, + }; +} diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 8a4b16fc5..9400f7aa3 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -59,7 +59,6 @@ import { moduleUsesMockTimers, } from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; -import { type DenseArrayLoop } from "./dense-array-loops.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { matchBytesLoopVersion } from "./bytes-loop-versions.js"; import { globalSlotAlias } from "./bytes-alias.js"; @@ -179,6 +178,7 @@ import { PublishWalkers } from "./publish.js"; import { computeImmutableFields, stableGlobals, type ImmutableFields } from "../immutability.js"; import { computePublishedTypes, type PublishedTypes } from "../publication.js"; import { emitExpr } from "./expr-dispatch.js"; +import { LOOPS } from "./loop-state.js"; import { dynKind, @@ -570,6 +570,8 @@ function mentionsLocal(e: IrExpr, localId: string): boolean { } export class LlEmitter { + /** Per-feature state (emitter-state.ts), indexed by each key's slot. */ + readonly moduleStates: unknown[] = []; localArrayReads = new Map(); loopArrayBorrows: ReadonlySet = new Set(); scalarStringSlices = new Map(); @@ -626,7 +628,7 @@ export class LlEmitter { * prefix: the counter of a dense array loop's fast copy indexing one of * the arrays its entry test covered. */ provenDenseIndex(arr: IrExpr, index: IrExpr): boolean { - const loop = this.denseLoop; + const loop = LOOPS.of(this).denseLoop; return ( loop !== null && arr.kind === "varRef" && @@ -1000,21 +1002,12 @@ export class LlEmitter { * immortal from construction (classes.ts programLifetimeClass). */ readonly programLifetime: ReadonlySet; numericLocals = new Map(); - /** Local arrays of the current function that no other binding can hold - * (ir/array-length-stability.ts). */ - unaliasedArrays: ReadonlySet = new Set(); - /** Dense array loops (dense-array-loops.ts) run in this function. */ - denseLoopsEnabled = false; - denseLoopEntries: ReturnType = new Map(); - /** The fast copy of a dense array loop being emitted. */ - denseLoop: DenseArrayLoop | null = null; captureIds = new Set(); /** Active induction bindings retain their width independently of size_t. */ integerLoopBindings = new Map< string, { slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } } >(); - countedLoopsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; /** The fast copy of a speculative integer loop being emitted: its @@ -1042,19 +1035,6 @@ export class LlEmitter { int32Elements: ((arr: IrExpr) => boolean) | null = null; /** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */ wrappingSums = new Set(); - byteWindowsEnabled = true; - byteWindowEntries: ReturnType = new Map(); - /** Typed-array loop versioning (bytes-loop-versions.ts) for the current - * function: off in debug and suspending functions and inside a copy. */ - bytesLoopVersionsEnabled = false; - /** Loops being versioned around the statement being emitted: a loop is - * never versioned inside its own copies, and versions nest boundedly. */ - readonly bytesLoopVersioning = new Set(); - bytesLoopEntries: ReturnType | null = null; - bytesLoopFunction: IrFunction | null = null; - /** Counted loops whose integer-form safety test an enclosing version - * guard already passed. */ - readonly provenCountedGuards = new Set(); bytesBounds: ReadonlySet = new Set(); integerViews = new Map(); /** Module globals the current function owns outright (initializer @@ -3391,10 +3371,10 @@ export class LlEmitter { s: IrStmt & { kind: "for" }, counted: IntegerCountedForLoop | null, ): ReturnType { - const fn = this.bytesLoopFunction; + const fn = LOOPS.of(this).bytesLoopFunction; if (!fn) return null; - this.bytesLoopEntries ??= findInitializedByteLoopBindings(fn); - const initialized = this.bytesLoopEntries.get(s); + LOOPS.of(this).bytesLoopEntries ??= findInitializedByteLoopBindings(fn); + const initialized = LOOPS.of(this).bytesLoopEntries!.get(s); if (!initialized) return null; const invariantGlobals = new Set(this.stableGlobals); for (const global of this.initializerBindings.get(fn.name) ?? []) diff --git a/packages/compiler/src/backend/llvm/loop-state.ts b/packages/compiler/src/backend/llvm/loop-state.ts new file mode 100644 index 000000000..ce9189a45 --- /dev/null +++ b/packages/compiler/src/backend/llvm/loop-state.ts @@ -0,0 +1,76 @@ +/* The loop optimizations' per-function state: counted loops, dense array + * loops, byte windows and typed-array loop versioning, and the unaliased + * local arrays they rely on. */ +import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; +import type { IrFunction, IrStmt } from "../../ir/ir.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import type { DenseArrayLoop } from "./dense-array-loops.js"; +import type { LlEmitter } from "./emitter.js"; +import { defineModuleState } from "./emitter-state.js"; + +/** Sets the loop state for `fn` at the start of its emission; `numericFn` + * is `fn` with its initializer bindings (emitFunction). */ +export function beginFunctionLoops( + emitter: LlEmitter, + fn: IrFunction, + numericFn: IrFunction, +): void { + const loops = LOOPS.of(emitter); + loops.unaliasedArrays = findUnaliasedLocalArrays(fn); + loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; + loops.byteWindowsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopEntries = null; + loops.bytesLoopFunction = fn; + loops.denseLoopsEnabled = loops.countedLoopsEnabled; + loops.denseLoop = null; + loops.denseLoopEntries = + loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") + ? findInitializedByteLoopBindings(fn) + : new Map(); + loops.byteWindowEntries = + loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") + ? findInitializedByteLoopBindings(fn) + : new Map(); +} + +/** The state LOOPS holds on each emitter. */ +export interface LoopState { + /** Local arrays of the current function that no other binding can hold + * (ir/array-length-stability.ts). */ + unaliasedArrays: ReadonlySet; + /** Dense array loops (dense-array-loops.ts) run in this function. */ + denseLoopsEnabled: boolean; + denseLoopEntries: ReturnType; + /** The fast copy of a dense array loop being emitted. */ + denseLoop: DenseArrayLoop | null; + countedLoopsEnabled: boolean; + byteWindowsEnabled: boolean; + byteWindowEntries: ReturnType; + /** Typed-array loop versioning (bytes-loop-versions.ts) for the current + * function: off in debug and suspending functions and inside a copy. */ + bytesLoopVersionsEnabled: boolean; + /** Loops being versioned around the statement being emitted: a loop is + * never versioned inside its own copies, and versions nest boundedly. */ + readonly bytesLoopVersioning: Set; + bytesLoopEntries: ReturnType | null; + bytesLoopFunction: IrFunction | null; + /** Counted loops whose integer-form safety test an enclosing version + * guard already passed. */ + readonly provenCountedGuards: Set; +} + +export const LOOPS = defineModuleState(() => ({ + unaliasedArrays: new Set(), + denseLoopsEnabled: false, + denseLoopEntries: new Map(), + denseLoop: null, + countedLoopsEnabled: false, + byteWindowsEnabled: true, + byteWindowEntries: new Map(), + bytesLoopVersionsEnabled: false, + bytesLoopVersioning: new Set(), + bytesLoopEntries: null, + bytesLoopFunction: null, + provenCountedGuards: new Set(), +})); diff --git a/scripts/split/README.md b/scripts/split/README.md new file mode 100644 index 000000000..7d79b35c1 --- /dev/null +++ b/scripts/split/README.md @@ -0,0 +1,22 @@ +# Split tools + +These scripts split a large source file into smaller files without changing behavior, and carry open branches across the split. Each transform is deterministic: the same config gives the same result on any version of the tree, and rehoming depends on that. + +Run them from a checkout with `node_modules` installed. That checkout is the tools directory (`--tools`) and supplies `typescript` and `oxfmt`. + +## Transforms +- `split-module.mjs plan|apply ` moves top-level declarations into new files. `plan` computes a manifest from seed declarations, and `apply` does the move. +- `split-methods.mjs apply ` moves class methods into exported functions that take the instance as a parameter (`emitter`). The class keeps a one-line stub with the same signature, so callers don't change. +- `move-state.mjs apply ` moves a group of class fields into a feature's state object, defined in the feature's file with `defineModuleState` (`packages/compiler/src/backend/llvm/emitter-state.ts`). It rewrites `this.` and `emitter.` to `KEY.of(this).` and creates files from templates. It also applies the change's other edits as literal replacements, in order. A replacement whose text isn't found the expected number of times is reported and skipped. + +Each transform takes `--repo ` (the tree to rewrite) and `--tools `, and formats what it writes with oxfmt. `configs/` holds the configs of splits that have landed. For example, `configs/loop-state/` moves the loop optimizations' state out of `LlEmitter`. + +## Rehoming an open branch +```sh +git fetch origin pull//head:pr- +node scripts/split/rehome-split.mjs --branch pr- --onto origin/main --config scripts/split/configs//.json +``` + +The result is the branch `pr--rehomed`, a merge of `origin/main` into the branch. + +The script applies the split's transform to the branch's merge base and to its tip, then cherry-picks the difference onto `--onto`. It records the result as a merge commit with parents (branch tip, `--onto`), so it can be pushed without rewriting the branch; `--squash` makes one ordinary commit instead. Conflicts made only of import lines in the split's files resolve to both sides' imports. Any other conflict stops the script, which lists the files and leaves its scratch worktree for you to resolve. diff --git a/scripts/split/configs/loop-state/emitter-state.template b/scripts/split/configs/loop-state/emitter-state.template new file mode 100644 index 000000000..62dc47b4e --- /dev/null +++ b/scripts/split/configs/loop-state/emitter-state.template @@ -0,0 +1,24 @@ +/* Per-feature emitter state. A feature defines its state in its own file + * with defineModuleState and reads it through the returned key, so adding + * or extending a feature does not edit LlEmitter's field list or + * constructor. */ +import type { LlEmitter } from "./emitter.js"; + +/** A feature's state on an emitter, read with `key.of(emitter)`. */ +export interface EmitterStateKey { + of(emitter: LlEmitter): T; +} + +let moduleSlots = 0; + +/** State that lives for the whole module, created on first read. */ +export function defineModuleState(create: (emitter: LlEmitter) => T): EmitterStateKey { + const slot = moduleSlots++; + return { + of(emitter) { + const states = emitter.moduleStates; + if (slot >= states.length || states[slot] === undefined) states[slot] = create(emitter); + return states[slot] as T; + }, + }; +} diff --git a/scripts/split/configs/loop-state/loop-state.config.json b/scripts/split/configs/loop-state/loop-state.config.json new file mode 100644 index 000000000..e749a4b39 --- /dev/null +++ b/scripts/split/configs/loop-state/loop-state.config.json @@ -0,0 +1,29 @@ +{ + "module": "packages/compiler/src/backend/llvm/emitter.ts", + "class": "LlEmitter", + "dir": "packages/compiler/src/backend/llvm", + "create": { + "emitter-state.ts": "emitter-state.template", + "loop-state.ts": "loop-state.template" + }, + "groups": [ + { + "file": "loop-state.ts", + "key": "LOOPS", + "interfaceName": "LoopState", + "fields": ["unaliasedArrays", "denseLoopsEnabled", "denseLoopEntries", "denseLoop", "countedLoopsEnabled", "byteWindowsEnabled", "byteWindowEntries", "bytesLoopVersionsEnabled", "bytesLoopVersioning", "bytesLoopEntries", "bytesLoopFunction", "provenCountedGuards"] + } + ], + "replacements": [ + { "file": "emitter.ts", "find": "export class LlEmitter {\n", "replace": "export class LlEmitter {\n /** Per-feature state (emitter-state.ts), indexed by each key's slot. */\n readonly moduleStates: unknown[] = [];\n" }, + { "file": "emitter.ts", "find": "import { type DenseArrayLoop } from \"./dense-array-loops.js\";\n", "replace": "" }, + { "file": "emitter.ts", "find": " const initialized = LOOPS.of(this).bytesLoopEntries.get(s);\n", "replace": " const initialized = LOOPS.of(this).bytesLoopEntries!.get(s);\n" }, + { "file": "emit-statements.ts", "find": " ? LOOPS.of(emitter).denseLoop.range\n", "replace": " ? LOOPS.of(emitter).denseLoop!.range\n", "count": 2 }, + { "file": "emit-function.ts", "find": " LOOPS.of(emitter).unaliasedArrays = findUnaliasedLocalArrays(fn);\n", "replace": "" }, + { "file": "emit-function.ts", "find": " LOOPS.of(emitter).countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;\n", "replace": " beginFunctionLoops(emitter, fn, numericFn);\n" }, + { "file": "emit-function.ts", "find": " LOOPS.of(emitter).byteWindowsEnabled = LOOPS.of(emitter).countedLoopsEnabled;\n LOOPS.of(emitter).bytesLoopVersionsEnabled = LOOPS.of(emitter).countedLoopsEnabled;\n LOOPS.of(emitter).bytesLoopEntries = null;\n LOOPS.of(emitter).bytesLoopFunction = fn;\n LOOPS.of(emitter).denseLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;\n LOOPS.of(emitter).denseLoop = null;\n LOOPS.of(emitter).denseLoopEntries =\n LOOPS.of(emitter).denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === \"array\")\n ? findInitializedByteLoopBindings(fn)\n : new Map();\n LOOPS.of(emitter).byteWindowEntries =\n LOOPS.of(emitter).byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === \"bytes\")\n ? findInitializedByteLoopBindings(fn)\n : new Map();\n", "replace": "" }, + { "file": "emit-function.ts", "find": "import { LOOPS } from \"./loop-state.js\";\n", "replace": "import { beginFunctionLoops, LOOPS } from \"./loop-state.js\";\n" }, + { "file": "emit-function.ts", "find": "import { findUnaliasedLocalArrays } from \"../../ir/array-length-stability.js\";\n", "replace": "" }, + { "file": "emit-function.ts", "find": "import { findInitializedByteLoopBindings } from \"./byte-windows.js\";\n", "replace": "" } + ] +} diff --git a/scripts/split/configs/loop-state/loop-state.template b/scripts/split/configs/loop-state/loop-state.template new file mode 100644 index 000000000..ae9673267 --- /dev/null +++ b/scripts/split/configs/loop-state/loop-state.template @@ -0,0 +1,34 @@ +/* The loop optimizations' per-function state: counted loops, dense array + * loops, byte windows and typed-array loop versioning, and the unaliased + * local arrays they rely on. */ +import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js"; +import type { IrFunction, IrStmt } from "../../ir/ir.js"; +import { findInitializedByteLoopBindings } from "./byte-windows.js"; +import type { DenseArrayLoop } from "./dense-array-loops.js"; +import type { LlEmitter } from "./emitter.js"; + +/** Sets the loop state for `fn` at the start of its emission; `numericFn` + * is `fn` with its initializer bindings (emitFunction). */ +export function beginFunctionLoops( + emitter: LlEmitter, + fn: IrFunction, + numericFn: IrFunction, +): void { + const loops = LOOPS.of(emitter); + loops.unaliasedArrays = findUnaliasedLocalArrays(fn); + loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; + loops.byteWindowsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled; + loops.bytesLoopEntries = null; + loops.bytesLoopFunction = fn; + loops.denseLoopsEnabled = loops.countedLoopsEnabled; + loops.denseLoop = null; + loops.denseLoopEntries = + loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array") + ? findInitializedByteLoopBindings(fn) + : new Map(); + loops.byteWindowEntries = + loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes") + ? findInitializedByteLoopBindings(fn) + : new Map(); +} diff --git a/scripts/split/move-state.mjs b/scripts/split/move-state.mjs new file mode 100644 index 000000000..5da787ac2 --- /dev/null +++ b/scripts/split/move-state.mjs @@ -0,0 +1,202 @@ +#!/usr/bin/env node +/* Move a group of LlEmitter fields into a feature's state object, as a + * deterministic rewrite (the same config gives the same result on any + * version of the tree, which the rehoming tool relies on). + * + * node move-state.mjs apply [--repo ] [--tools ] + * + * : + * { "module": "packages/compiler/src/backend/llvm/emitter.ts", + * "class": "LlEmitter", + * "dir": "packages/compiler/src/backend/llvm", + * "groups": [{ "file": "loop-state.ts", "key": "LOOPS", + * "fields": ["denseLoop", ...] }], + * "create": { "loop-state.ts": "loop-state.template.ts" }, + * "replacements": [{ "file": "emit-function.ts", "find": "...", "replace": "...", "count": 1 }], + * "receivers": ["this", "emitter"] } + * + * For each group: the fields' declarations leave the class (their doc + * comments and initializers move into the state object's interface and + * factory in the group's file), every `this.` and `emitter.` + * (or the config's receivers) in the directory's sources becomes + * `.of(this).` / `.of(emitter).`, and each + * rewritten file imports the key. + * The group's file must already exist and hold everything else (header, + * setup function); the tool appends the interface and key to it. */ +import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { createRequire } from "node:module"; +import { execFileSync } from "node:child_process"; + +const [command, configPath, ...flags] = process.argv.slice(2); +if (command !== "apply") throw new Error("usage: move-state.mjs apply "); +const config = JSON.parse(readFileSync(configPath, "utf8")); +const flag = (name) => { + const i = flags.indexOf(name); + return i >= 0 ? flags[i + 1] : undefined; +}; +const repo = resolve(flag("--repo") ?? process.cwd()); +const toolsDir = resolve(flag("--tools") ?? process.cwd()); +const require = createRequire(join(toolsDir, "package.json")); +const ts = require("typescript"); +const modulePath = join(repo, config.module); +const dir = join(repo, config.dir); + +// Adds an import line after the file's first block of imports (up to a +// blank line or other code). +const addImport = (text, line) => { + const sf = ts.createSourceFile("x.ts", text, ts.ScriptTarget.Latest, true); + let last; + for (const s of sf.statements) { + if (!ts.isImportDeclaration(s)) break; + if (last && /\n[ \t]*\n/.test(text.slice(last.getEnd(), s.getStart()))) break; + last = s; + } + const at = last ? last.getEnd() + 1 : 0; + return text.slice(0, at) + line + "\n" + text.slice(at); +}; +const configDir = dirname(resolve(configPath)); +// The names the class's instance goes by in the directory's sources. +const receivers = config.receivers ?? ["this", "emitter"]; +const touched = new Set(); +// Files the change creates, from templates next to the config (kept when a +// branch already has the file). +for (const [file, template] of Object.entries(config.create ?? {})) { + const path = join(dir, file); + if (existsSync(path)) continue; + writeFileSync(path, readFileSync(join(configDir, template), "utf8")); + touched.add(path); +} +let moduleText = readFileSync(modulePath, "utf8"); +for (const group of config.groups) { + const sf = ts.createSourceFile(modulePath, moduleText, ts.ScriptTarget.Latest, true); + const cls = sf.statements.find((s) => ts.isClassDeclaration(s) && s.name?.text === config.class); + if (!cls) throw new Error(`no class ${config.class}`); + const wanted = new Set(group.fields); + const members = []; + for (const m of cls.members) { + if (ts.isPropertyDeclaration(m) && ts.isIdentifier(m.name) && wanted.has(m.name.text)) + members.push(m); + } + // A field the tree does not have yet (an older merge base) is left out, so + // the move still applies there. + const missing = group.fields.filter((f) => !members.some((m) => m.name.text === f)); + if (missing.length) + console.warn(`${group.file}: fields not in ${config.class}: ${missing.join(", ")}; skipped`); + group.present = members.map((m) => m.name.text); + // Interface members and factory entries, in the class's order. + const iface = []; + const factory = []; + for (const m of members) { + const lead = moduleText.slice(m.getFullStart(), m.getStart()).replace(/^\s*\n/, ""); + const readonly = m.modifiers?.some((x) => x.kind === ts.SyntaxKind.ReadonlyKeyword) + ? "readonly " + : ""; + const name = m.name.text; + if (!m.initializer) + throw new Error( + `${name}: a field without an initializer needs its constructor line moved by hand`, + ); + // An unannotated field takes its initializer's type: a boolean, + // number or string literal, or `new C(...)`. + const init = m.initializer; + const type = m.type + ? m.type.getText(sf) + : init.kind === ts.SyntaxKind.TrueKeyword || init.kind === ts.SyntaxKind.FalseKeyword + ? "boolean" + : ts.isNumericLiteral(init) + ? "number" + : ts.isStringLiteral(init) + ? "string" + : ts.isNewExpression(init) && init.typeArguments?.length + ? `${init.expression.getText(sf)}<${init.typeArguments.map((t) => t.getText(sf)).join(", ")}>` + : null; + if (type === null) throw new Error(`${name}: give the field a type annotation first`); + iface.push(`${lead} ${readonly}${name}: ${type};`); + factory.push(` ${name}: ${m.initializer.getText(sf)},`); + } + // Remove the declarations (with their leading comments) from the class. + const cuts = members.map((m) => [m.getFullStart(), m.getEnd()]).sort((a, b) => b[0] - a[0]); + for (const [s, e] of cuts) moduleText = moduleText.slice(0, s) + moduleText.slice(e); + const iName = group.interface ?? `${group.key[0]}${group.key.slice(1).toLowerCase()}State`; + const stateText = + `\n/** The state ${group.key} holds on each emitter. */\n` + + `export interface ${group.interfaceName ?? iName} {\n${iface.join("\n")}\n}\n\n` + + `export const ${group.key} = defineModuleState<${group.interfaceName ?? iName}>(() => ({\n${factory.join("\n")}\n}));\n`; + const groupPath = join(dir, group.file); + let groupText = readFileSync(groupPath, "utf8"); + if (!groupText.includes('from "./emitter-state.js"')) + groupText = addImport(groupText, `import { defineModuleState } from "./emitter-state.js";`); + groupText = groupText.trimEnd() + "\n" + stateText; + writeFileSync(groupPath, groupText); + touched.add(groupPath); + group.rename = group.present.length + ? new RegExp(`(? f.endsWith(".ts"))) { + const path = join(dir, file); + let text = path === modulePath ? moduleText : readFileSync(path, "utf8"); + let changed = false; + for (const group of config.groups) { + if (!group.rename) continue; + const next = text.replace( + group.rename, + (_, recv, field) => `${group.key}.of(${recv}).${field}`, + ); + if (next === text) continue; + text = next; + changed = true; + if (file === group.file) continue; + const spec = `./${group.file.replace(/\.ts$/, ".js")}`; + const existing = new RegExp( + `^import \\{([^}]*)\\} from "${spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";$`, + "m", + ); + const m = existing.exec(text); + if (m) { + const names = new Set( + m[1] + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + ); + names.add(group.key); + text = text.replace(existing, `import { ${[...names].sort().join(", ")} } from "${spec}";`); + } else { + text = addImport(text, `import { ${group.key} } from "${spec}";`); + } + } + if (changed) { + writeFileSync(path, text); + touched.add(path); + } +} +// The change's remaining edits, as literal replacements in order (the +// setup function call, imports, narrowing the rename loses). One whose text +// is not found as often as expected is reported and skipped: the branch +// changed that code, and the rehome's cherry-pick shows the difference. +let skipped = 0; +for (const r of config.replacements ?? []) { + const path = join(dir, r.file); + const text = existsSync(path) ? readFileSync(path, "utf8") : ""; + const count = text.split(r.find).length - 1; + if (count !== (r.count ?? 1)) { + console.warn( + `${r.file}: expected ${r.count ?? 1} of ${JSON.stringify(r.find.slice(0, 60))}, found ${count}; skipped`, + ); + skipped++; + continue; + } + writeFileSync(path, text.split(r.find).join(r.replace)); + touched.add(path); +} +const oxfmt = join(toolsDir, "node_modules/.bin/oxfmt"); +execFileSync(oxfmt, [...touched], { cwd: repo, stdio: "inherit" }); +if (skipped) console.warn(`${skipped} replacement(s) skipped`); +console.log( + `moved ${config.groups.map((g) => g.present.length).reduce((a, b) => a + b, 0)} fields; rewrote ${touched.size} files`, +); diff --git a/scripts/split/rehome-split.mjs b/scripts/split/rehome-split.mjs new file mode 100644 index 000000000..2884558fb --- /dev/null +++ b/scripts/split/rehome-split.mjs @@ -0,0 +1,311 @@ +#!/usr/bin/env node +/* Rehome a branch's changes across a module split. + * + * A split moves whole declarations out of one module into new files, so git + * cannot follow a branch's edits into them: merging the split reports every + * edit to a moved declaration as a conflict. This script carries the branch + * across instead. It applies the same split (split-module.mjs, + * split-methods.mjs or move-state.mjs with the split's config; each also + * formats with oxfmt) to the branch's merge base and to the branch tip, and + * cherry-picks the difference onto the target. That is a three-way merge + * whose base and branch side already use the new layout, so each hunk lands + * in the file its declaration moved to. Hunks in declarations that stay, and + * declarations the branch adds, stay in the module. + * + * node rehome-split.mjs --branch --onto --config + * [--out ] [--squash] [--scratch ] + * + * Run it from a checkout with node_modules installed (it supplies typescript + * and oxfmt). --onto is a commit that contains the split, usually main. + * The result is a new branch (default -rehomed). By default its tip + * is a merge commit with parents and : the branch's history + * is kept and the result reads as "merge main into the branch", so it can be + * pushed without rewriting the branch. --squash makes one ordinary commit on + * instead. When the cherry-pick conflicts, the script stops and + * leaves the work tree with the conflicts to resolve; see the message. */ +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const args = process.argv.slice(2); +const opt = (name, fallback) => { + const i = args.indexOf(`--${name}`); + return i >= 0 ? args[i + 1] : fallback; +}; +const has = (name) => args.includes(`--${name}`); +const branch = opt("branch"); +const onto = opt("onto"); +const configPath = opt("config"); +if (!branch || !onto || !configPath) { + console.error( + "usage: rehome-split.mjs --branch --onto --config [--out ] [--squash]", + ); + process.exit(2); +} +const out = opt("out", `${branch.replace(/^.*\//, "")}-rehomed`); +const tools = process.cwd(); +const here = dirname(fileURLToPath(import.meta.url)); +const config = JSON.parse(readFileSync(configPath, "utf8")); +// A config with "groups" moves fields into feature state (move-state.mjs), +// one with "methods" moves methods (split-methods.mjs), and any other moves +// top-level declarations (split-module.mjs). +const splitTool = join( + here, + config.groups ? "move-state.mjs" : config.class ? "split-methods.mjs" : "split-module.mjs", +); + +const git = (...a) => + execFileSync("git", a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); +const gitIn = (cwd, ...a) => + execFileSync("git", a, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); +// Every commit the script makes is authored and committed as the branch's +// last author. +const author = (format) => git("log", "-1", `--format=${format}`, branch); +const ident = { + GIT_AUTHOR_NAME: author("%an"), + GIT_AUTHOR_EMAIL: author("%ae"), +}; +ident.GIT_COMMITTER_NAME = ident.GIT_AUTHOR_NAME; +ident.GIT_COMMITTER_EMAIL = ident.GIT_AUTHOR_EMAIL; +Object.assign(process.env, ident); + +const tip = git("rev-parse", `${branch}^{commit}`); +const target = git("rev-parse", `${onto}^{commit}`); +// The split must not be in the branch already, or there is nothing to rehome. +const moduleFile = config.module; +// With several merge bases (criss-cross history, such as a branch that +// carries another branch's commits that main merged), merge them into one +// virtual base, as git's merge does, so the cherry-pick sees the same base. +let base = git("merge-base", tip, target); +{ + const bases = git("merge-base", "--all", tip, target).split("\n").filter(Boolean); + for (const next of bases.slice(1)) { + const merged = spawnSync("git", ["merge-tree", "--write-tree", base, next], { + encoding: "utf8", + }); + const tree = merged.stdout.split("\n")[0]; + base = git("commit-tree", tree, "-p", base, "-p", next, "-m", "virtual merge base"); + } +} + +// Scratch worktrees go next to the checkout: not inside it (they would show +// as untracked files) and not under node_modules (oxfmt skips those paths). +const scratch = mkdtempSync(opt("scratch", join(dirname(tools), ".rehome-"))); +const worktree = (name, commit) => { + const dir = join(scratch, name); + git("worktree", "add", "--detach", "-q", dir, commit); + return dir; +}; +const split = (dir) => { + execFileSync( + process.execPath, + [splitTool, "apply", configPath, "--repo", dir, "--tools", tools], + { + stdio: ["ignore", "pipe", "inherit"], + }, + ); + gitIn(dir, "add", "-A"); + return gitIn(dir, "write-tree"); +}; +const commitTree = (tree, parents, message) => + execFileSync("git", ["commit-tree", tree, ...parents.flatMap((p) => ["-p", p]), "-m", message], { + encoding: "utf8", + env: { + ...process.env, + ...ident, + }, + }).trim(); + +let baseDir, tipDir; +try { + baseDir = worktree("base", base); + tipDir = worktree("tip", tip); + const splitBase = commitTree(split(baseDir), [base], "split of the merge base"); + const splitTip = commitTree(split(tipDir), [splitBase], "split of the branch tip"); + if (git("rev-parse", `${splitBase}^{tree}`) === git("rev-parse", `${splitTip}^{tree}`)) { + console.log(`${branch} has no changes relative to ${base.slice(0, 9)}`); + process.exit(0); + } + const touched = git("diff", "--name-only", splitBase, splitTip).split("\n").filter(Boolean); + console.log(`${branch}: ${touched.length} files changed in the split layout`); + // Apply the branch's changes onto the target in a worktree of its own. + const resultDir = worktree("result", target); + const pick = spawnSync( + "git", + ["-c", "merge.conflictStyle=merge", "cherry-pick", "--no-commit", splitTip], + { + cwd: resultDir, + encoding: "utf8", + }, + ); + // Moved declarations bring short import blocks, where an import main added + // and one the branch added can sit on adjacent lines. Resolve conflicts + // made only of import declarations to the union of both sides. + if (pick.status !== 0) { + // Only the files the split writes: other conflicts are the branch's own. + // A method split names its files in the config; a module split, in its manifest. + const files = config.groups + ? [ + ...config.groups.map((g) => g.file), + ...Object.keys(config.create ?? {}), + ...(config.replacements ?? []).map((r) => r.file), + ] + : config.class + ? Object.keys(config.methods) + : Object.keys( + (({ files, ...rest }) => files ?? rest)( + JSON.parse( + readFileSync(resolve(dirname(resolve(configPath)), config.manifest), "utf8"), + ), + ), + ); + const moduleDir = config.dir ?? dirname(moduleFile); + const splitFiles = new Set([moduleFile, ...files.map((f) => join(moduleDir, f))]); + for (const file of gitIn(resultDir, "diff", "--name-only", "--diff-filter=U") + .split("\n") + .filter(Boolean)) { + if (splitFiles.has(file) && resolveImportConflicts(join(resultDir, file))) { + execFileSync(join(tools, "node_modules/.bin/oxfmt"), [file], { + cwd: resultDir, + stdio: ["ignore", "ignore", "inherit"], + }); + gitIn(resultDir, "add", file); + console.log(`${file}: import conflicts resolved to both sides' imports`); + } + } + } + if (pick.status !== 0 && gitIn(resultDir, "diff", "--name-only", "--diff-filter=U") !== "") { + const conflicts = gitIn(resultDir, "diff", "--name-only", "--diff-filter=U"); + console.error( + `conflicts in:\n${conflicts}\n\nResolve them in ${resultDir}, then run:\n git -C ${resultDir} add -A && git -C ${resultDir} commit -m "Merge ${onto} into ${branch}" # or rerun with the resolution\nThe scratch worktrees stay until you remove ${scratch}.`, + ); + process.exit(1); + } + const tree = gitIn(resultDir, "write-tree"); + const subjects = git("log", "--format=%s", `${base}..${tip}`).split("\n").filter(Boolean); + const result = has("squash") + ? commitTree( + tree, + [target], + subjects.length === 1 + ? subjects[0] + : `${subjects.at(-1)}\n\n${subjects.map((s) => `- ${s}`).join("\n")}`, + ) + : commitTree( + tree, + [tip, target], + `Merge ${onto} into ${branch.replace(/^.*\//, "")}\n\nCarries the branch's changes to ${moduleFile} into the files its declarations moved to.`, + ); + git("branch", "-f", out, result); + console.log( + `${out} -> ${result.slice(0, 9)} (${has("squash") ? "one commit on" : "merge of"} ${onto})`, + ); + for (const dir of [baseDir, tipDir, resultDir]) git("worktree", "remove", "--force", dir); + rmSync(scratch, { recursive: true, force: true }); +} catch (error) { + console.error(error.stderr?.toString() || error.message); + console.error(`scratch worktrees left in ${scratch}`); + process.exit(1); +} + +/* Rewrites every conflict in `path` whose two sides hold only import + * declarations as the union of both: one declaration per module and kind, + * with the named imports of both sides in first-seen order. Returns false, + * leaving the file unchanged, when any conflict holds other code. */ +function resolveImportConflicts(path) { + const ts = createRequire(join(tools, "package.json"))("typescript"); + const text = readFileSync(path, "utf8"); + const region = /^<<<<<<< [^\n]*\n([\s\S]*?)^=======\n([\s\S]*?)^>>>>>>> [^\n]*\n/gm; + const imports = (side) => { + const sf = ts.createSourceFile("side.ts", side, ts.ScriptTarget.Latest, true); + return sf.statements.every(ts.isImportDeclaration) ? sf.statements : null; + }; + let ok = true; + const imported = new Set(); + const resolved = text.replace(region, (whole, ours, theirs) => { + const a = imports(ours), + b = imports(theirs); + if (!a || !b) { + ok = false; + return whole; + } + const merged = new Map(); + for (const decl of [...a, ...b]) { + const clause = decl.importClause; + const named = + clause?.namedBindings && ts.isNamedImports(clause.namedBindings) + ? clause.namedBindings + : null; + if (!clause || !named || clause.name) { + // Side-effect, default and namespace imports are kept as written, once. + merged.set(decl.getText(), null); + continue; + } + const key = `${clause.isTypeOnly ? "type " : ""}${decl.moduleSpecifier.getText()}`; + const entry = merged.get(key) ?? { + typeOnly: clause.isTypeOnly, + from: decl.moduleSpecifier.getText(), + names: [], + }; + for (const element of named.elements) { + imported.add(element.name.text); + if (!entry.names.includes(element.getText())) entry.names.push(element.getText()); + } + merged.set(key, entry); + } + return ( + [...merged] + .map(([key, entry]) => + entry === null + ? key + : `import ${entry.typeOnly ? "type " : ""}{ ${entry.names.join(", ")} } from ${entry.from};`, + ) + .join("\n") + "\n" + ); + }); + if (!ok || resolved === text) return false; + writeFileSync(path, pruneUnusedImports(ts, resolved, imported)); + return true; +} + +/* Drops the named imports among `names` that nothing outside import + * declarations references: a side's import whose use the other side's code + * replaced. The split computes each file's imports from its references, so + * this keeps a rehomed file's imports the ones the split would write. */ +function pruneUnusedImports(ts, text, names) { + const sf = ts.createSourceFile("file.ts", text, ts.ScriptTarget.Latest, true); + const used = new Set(); + const visit = (node) => { + if (ts.isImportDeclaration(node)) return; + if (ts.isIdentifier(node)) used.add(node.text); + ts.forEachChild(node, visit); + }; + visit(sf); + const edits = []; + for (const decl of sf.statements) { + const named = decl.importClause?.namedBindings; + if ( + !ts.isImportDeclaration(decl) || + !named || + !ts.isNamedImports(named) || + decl.importClause.name + ) + continue; + const kept = named.elements.filter((e) => !names.has(e.name.text) || used.has(e.name.text)); + if (kept.length === named.elements.length) continue; + const from = decl.moduleSpecifier.getText(); + const prefix = decl.importClause.isTypeOnly ? "import type" : "import"; + // A dropped declaration takes its line break with it. + const end = kept.length || text[decl.getEnd()] !== "\n" ? decl.getEnd() : decl.getEnd() + 1; + edits.push([ + decl.getStart(), + end, + kept.length ? `${prefix} { ${kept.map((e) => e.getText()).join(", ")} } from ${from};` : "", + ]); + } + for (const [start, end, replacement] of edits.reverse()) + text = text.slice(0, start) + replacement + text.slice(end); + return text; +} diff --git a/scripts/split/split-methods.mjs b/scripts/split/split-methods.mjs new file mode 100644 index 000000000..66d6b36ad --- /dev/null +++ b/scripts/split/split-methods.mjs @@ -0,0 +1,414 @@ +#!/usr/bin/env node +/* Move methods of one class into other files as functions of a host, as a + * pure move. + * + * node split-methods.mjs apply [--repo ] [--tools ] + * + * : + * { "tsconfig": "packages/compiler/tsconfig.json", + * "module": "packages/compiler/src/backend/llvm/emitter.ts", + * "class": "LlEmitter", + * "methods": { "emit-statements.ts": ["emitStmtBody", ...], ... }, + * "headers": { "emit-statements.ts": "/* ... *\/" } } + * + * Each listed method becomes `export function (host: , ...)` + * in its file (relative to the module's directory), with its doc comment + * and `this` replaced by the host parameter (`emitter`, or "hostName"). The class keeps a stub with the method's + * signature that calls the function, so every caller is unchanged. Members + * the moved bodies reach through `host` lose a `private` modifier (a + * compile-time check only). Each new file imports what its functions + * reference from the module's scope, with paths rebased; the module imports + * the functions and drops imports nothing uses anymore. Names the config + * lists but the class lacks are skipped. */ +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { dirname, join, relative, resolve, posix } from "node:path"; +import { createRequire } from "node:module"; +import { execFileSync } from "node:child_process"; + +const [command, configPath, ...flags] = process.argv.slice(2); +if (command !== "apply") throw new Error("usage: split-methods.mjs apply "); +const config = JSON.parse(readFileSync(configPath, "utf8")); +const flag = (name) => { + const i = flags.indexOf(name); + return i >= 0 ? flags[i + 1] : undefined; +}; +const repo = resolve(flag("--repo") ?? process.cwd()); +const toolsDir = resolve(flag("--tools") ?? process.cwd()); +const require = createRequire(join(toolsDir, "package.json")); +const ts = require("typescript"); +const modulePath = join(repo, config.module); +const moduleDir = dirname(modulePath); +const className = config.class; +// The host parameter's name; a moved body that already uses it is refused. +const hostName = config.hostName ?? "emitter"; + +const text = readFileSync(modulePath, "utf8"); +const host = ts.createCompilerHost({ noEmit: true }); +const sf = ts.createSourceFile(modulePath, text, ts.ScriptTarget.Latest, true); +const realGet = host.getSourceFile; +host.getSourceFile = (f, l, ...r) => + resolve(f) === modulePath ? sf : realGet.call(host, f, l, ...r); +const program = ts.createProgram([modulePath], { noEmit: true, noResolve: true }, host); +const checker = program.getTypeChecker(); +const source = program.getSourceFile(modulePath); + +const cls = source.statements.find((s) => ts.isClassDeclaration(s) && s.name?.text === className); +if (!cls) throw new Error(`no class ${className}`); +const memberByName = new Map(); +for (const m of cls.members) + if (m.name && ts.isIdentifier(m.name)) { + if (!memberByName.has(m.name.text)) memberByName.set(m.name.text, []); + memberByName.get(m.name.text).push(m); + } + +// Module scope: import bindings and top-level declarations. +const imports = new Map(); +const topLevel = new Map(); +for (const st of source.statements) { + if (ts.isImportDeclaration(st)) { + const c = st.importClause; + if (!c) continue; + if (c.name) imports.set(c.name.text, { st, kind: "default", typeOnly: c.isTypeOnly }); + const nb = c.namedBindings; + if (nb && ts.isNamespaceImport(nb)) + imports.set(nb.name.text, { st, kind: "namespace", typeOnly: c.isTypeOnly }); + if (nb && ts.isNamedImports(nb)) + for (const el of nb.elements) + imports.set(el.name.text, { + st, + kind: "named", + typeOnly: c.isTypeOnly || el.isTypeOnly, + imported: (el.propertyName ?? el.name).text, + }); + continue; + } + const names = []; + if ( + (ts.isFunctionDeclaration(st) || + ts.isClassDeclaration(st) || + ts.isInterfaceDeclaration(st) || + ts.isTypeAliasDeclaration(st) || + ts.isEnumDeclaration(st)) && + st.name + ) + names.push(st.name.text); + if (ts.isVariableStatement(st)) + for (const d of st.declarationList.declarations) + if (ts.isIdentifier(d.name)) names.push(d.name.text); + for (const n of names) topLevel.set(n, st); +} +const isExported = (st) => + st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; +const isTypeDecl = (st) => ts.isInterfaceDeclaration(st) || ts.isTypeAliasDeclaration(st); + +/** Module-scope references in `node`: name -> { kind: "import"|"local" }, and `this.` names. */ +function references(node) { + const refs = new Map(); + const members = new Set(); + const visit = (n) => { + if ( + n.kind === ts.SyntaxKind.ThisKeyword && + n.parent && + ts.isPropertyAccessExpression(n.parent) && + n.parent.expression === n + ) + members.add(n.parent.name.text); + if (ts.isIdentifier(n)) { + const p = n.parent; + const propName = + p && + ((ts.isPropertyAccessExpression(p) && p.name === n) || + (ts.isQualifiedName(p) && p.right === n) || + (ts.isPropertyAssignment(p) && p.name === n)); + if (!propName) { + let sym = checker.getSymbolAtLocation(n); + if (p && ts.isShorthandPropertyAssignment(p)) + sym = checker.getShorthandAssignmentValueSymbol(p); + const decl = sym?.declarations?.[0]; + if (decl && decl.getSourceFile() === source) { + if (ts.isImportSpecifier(decl) || ts.isNamespaceImport(decl) || ts.isImportClause(decl)) + refs.set(decl.name.text, { kind: "import" }); + else if ( + topLevel.get(n.text) && + (decl === topLevel.get(n.text) || + decl.parent?.parent === topLevel.get(n.text) || + decl.parent === topLevel.get(n.text)) + ) + refs.set(n.text, { kind: "local" }); + } + } + } + ts.forEachChild(n, visit); + }; + visit(node); + return { refs, members }; +} + +const rebase = (spec, toDir) => { + if (!spec.startsWith(".")) return spec; + let rel = posix.normalize(relative(toDir, resolve(moduleDir, spec)).split("\\").join("/")); + return rel.startsWith(".") ? rel : "./" + rel; +}; +const specTo = (fromDir, file) => { + let rel = relative(fromDir, file).split("\\").join("/").replace(/\.ts$/, ".js"); + return rel.startsWith(".") ? rel : "./" + rel; +}; + +const edits = []; // [start, end, replacement] in the module +const stubs = []; +const files = new Map(); +const publicize = new Set(); +const moduleNeeds = new Map(); // spec -> names (functions the stubs call) +const exportLocals = new Set(); +let moved = 0; + +for (const [file, names] of Object.entries(config.methods)) { + const path = join(moduleDir, file); + const dir = dirname(path); + const entry = { decls: [], named: new Map(), other: [] }; + const addNamed = (spec, name, imported, typeOnly) => { + if (!entry.named.has(spec)) entry.named.set(spec, new Map()); + const m = entry.named.get(spec); + const prev = m.get(name); + m.set(name, { imported, typeOnly: prev ? prev.typeOnly && typeOnly : typeOnly }); + }; + addNamed(specTo(dir, modulePath), className, className, true); + for (const name of names) { + const decls = memberByName.get(name); + if (!decls) { + console.error(`skip: ${className}.${name} not found`); + continue; + } + if (decls.length !== 1 || !ts.isMethodDeclaration(decls[0]) || !decls[0].body) + throw new Error(`${name}: expected one method with a body`); + const m = decls[0]; + if (m.typeParameters?.length || m.asteriskToken) + throw new Error(`${name}: generic and generator methods are not handled`); + const isAsync = m.modifiers?.some((x) => x.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + // The function: doc comment, signature (initializers kept, so a + // defaulted parameter after an optional one stays valid; the stub + // passes every argument), and the body with this -> host. + const lead = text.slice(m.getFullStart(), m.getStart()).replace(/^\s*\n/, ""); + const params = m.parameters.map((p) => { + const nameText = p.name.getText(); + // A parameter typed only by its default takes the default's widened type. + const type = p.type + ? `: ${p.type.getText()}` + : p.initializer + ? `: ${checker.typeToString(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(p.initializer)))}` + : ""; + const optional = p.questionToken ? "?" : ""; + let init = ""; + if (p.initializer) { + const initText = p.initializer.getText(); + if (/\bthis\b/.test(initText)) + throw new Error(`${name}: a parameter default that reads this is not handled`); + init = ` = ${initText}`; + } + return `${p.dotDotDotToken ? "..." : ""}${nameText}${optional}${type}${init}`; + }); + const ret = m.type ? `: ${m.type.getText()}` : ""; + const body = m.body; + const thisEdits = []; + const findThis = (n) => { + // `this` in an expression, or in a type query (`typeof this.x`). + // (In a type query `this` parses as an identifier.) + if ( + n.kind === ts.SyntaxKind.ThisKeyword || + (ts.isIdentifier(n) && n.text === "this" && ts.findAncestor(n, ts.isTypeQueryNode)) + ) + thisEdits.push(n.getStart()); + if (ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n) || ts.isClassLike(n)) + throw new Error(`${name}: a nested function or class rebinds this`); + ts.forEachChild(n, findThis); + }; + ts.forEachChild(body, findThis); + const clash = (n) => { + if (ts.isIdentifier(n) && n.text === hostName) + throw new Error(`${name}: already uses the name ${hostName}`); + ts.forEachChild(n, clash); + }; + clash(m); + let bodyText = ""; + let cursor = body.getStart(); + for (const at of thisEdits.sort((a, b) => a - b)) { + bodyText += text.slice(cursor, at) + hostName; + cursor = at + 4; + } + bodyText += text.slice(cursor, body.end); + const leadOut = lead + .split("\n") + .map((l) => (l.startsWith(" ") ? l.slice(2) : l)) + .join("\n"); + const fn = `${leadOut}export ${isAsync ? "async " : ""}function ${name}(${hostName}: ${className}${params.length ? ", " + params.join(", ") : ""})${ret} ${bodyText}`; + entry.decls.push(fn); + stubs.push(m); + if (!moduleNeeds.has(specTo(moduleDir, path))) + moduleNeeds.set(specTo(moduleDir, path), new Set()); + moduleNeeds.get(specTo(moduleDir, path)).add(name); + // What the function needs. + const { refs, members } = references(m); + for (const member of members) publicize.add(member); + for (const [ref, { kind }] of refs) { + if (kind === "import") { + const imp = imports.get(ref); + const spec = rebase(imp.st.moduleSpecifier.text, dir); + if (imp.kind === "named") addNamed(spec, ref, imp.imported, imp.typeOnly); + else entry.other.push({ spec, kind: imp.kind, name: ref, typeOnly: imp.typeOnly }); + } else { + const st = topLevel.get(ref); + if (!isExported(st)) exportLocals.add(st); + addNamed(specTo(dir, modulePath), ref, ref, isTypeDecl(st)); + } + } + moved++; + } + files.set(file, entry); +} + +const renderImports = (entry) => { + const lines = []; + const seen = new Set(); + for (const o of entry.other) { + const key = `${o.kind}:${o.name}:${o.spec}`; + if (seen.has(key)) continue; + seen.add(key); + lines.push( + o.kind === "namespace" + ? `import ${o.typeOnly ? "type " : ""}* as ${o.name} from "${o.spec}";` + : `import ${o.typeOnly ? "type " : ""}${o.name} from "${o.spec}";`, + ); + } + for (const [spec, names] of entry.named) { + const all = [...names.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); + const allType = all.every(([, v]) => v.typeOnly); + const parts = all.map( + ([n, v]) => + `${!allType && v.typeOnly ? "type " : ""}${v.imported === n ? n : `${v.imported} as ${n}`}`, + ); + lines.push(`import ${allType ? "type " : ""}{ ${parts.join(", ")} } from "${spec}";`); + } + return lines; +}; + +for (const [file, entry] of files) { + if (!entry.decls.length) continue; + const path = join(moduleDir, file); + if (existsSync(path)) throw new Error(`${file} exists`); + const header = config.headers?.[file]; + writeFileSync( + path, + [...(header ? [header] : []), ...renderImports(entry), "", entry.decls.join("\n\n"), ""].join( + "\n", + ), + ); +} + +// Each moved method leaves a stub with its signature (defaults included) +// that calls the function; one the moved bodies call loses `private`. +const stubSet = new Set(stubs); +for (const m of stubs) { + let signature = m.modifiers + ? text.slice(m.getStart(), m.body.getStart()).trimEnd() + : text.slice(m.getStart(), m.body.getStart()).trimEnd(); + if (publicize.has(m.name.text)) signature = signature.replace(/^private /, ""); + const args = m.parameters.map((p) => `${p.dotDotDotToken ? "..." : ""}${p.name.getText()}`); + const call = `${m.name.text}(this${args.length ? ", " + args.join(", ") : ""})`; + edits.push([m.getFullStart(), m.end, `\n ${signature} {\n return ${call};\n }`]); +} +// Other members the moved bodies reach through the host lose `private`. +for (const name of publicize) + for (const m of memberByName.get(name) ?? []) + if (!stubSet.has(m)) + for (const mod of m.modifiers ?? []) + if (mod.kind === ts.SyntaxKind.PrivateKeyword) + edits.push([mod.getStart(), mod.end + 1, ""]); +// Top-level declarations the new files import get `export`. +for (const st of exportLocals) edits.push([st.getStart(), st.getStart(), "export "]); + +let out = ""; +let cursor = 0; +for (const [a, b, r] of edits.sort((x, y) => x[0] - y[0])) { + out += text.slice(cursor, a) + r; + cursor = b; +} +out += text.slice(cursor); + +// Re-analyze the rewritten module for unused imports, then add the new ones. +const sf2 = ts.createSourceFile(modulePath, out, ts.ScriptTarget.Latest, true); +const host2 = ts.createCompilerHost({ noEmit: true }); +const get2 = host2.getSourceFile; +host2.getSourceFile = (f, l, ...r) => + resolve(f) === modulePath ? sf2 : get2.call(host2, f, l, ...r); +const program2 = ts.createProgram([modulePath], { noEmit: true, noResolve: true }, host2); +const checker2 = program2.getTypeChecker(); +const src2 = program2.getSourceFile(modulePath); +const used = new Set(); +const walk = (n) => { + if ( + ts.isIdentifier(n) && + !(n.parent && ts.isImportSpecifier(n.parent)) && + !(n.parent && ts.isImportClause(n.parent)) && + !(n.parent && ts.isNamespaceImport(n.parent)) + ) { + const sym = checker2.getSymbolAtLocation(n); + const d = sym?.declarations?.[0]; + if (d && (ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d))) + used.add(d.name.text); + } + ts.forEachChild(n, walk); +}; +for (const st of src2.statements) if (!ts.isImportDeclaration(st)) walk(st); +const importEdits = []; +for (const st of src2.statements) { + if (!ts.isImportDeclaration(st) || !st.importClause) continue; + const c = st.importClause; + const def = c.name && used.has(c.name.text) ? c.name.text : null; + const nb = c.namedBindings; + const ns = nb && ts.isNamespaceImport(nb) && used.has(nb.name.text) ? nb.name.text : null; + const named = + nb && ts.isNamedImports(nb) ? nb.elements.filter((el) => used.has(el.name.text)) : []; + const all = + (c.name ? def !== null : true) && + (!nb || (ts.isNamespaceImport(nb) ? ns !== null : named.length === nb.elements.length)); + if (all) continue; + let r = ""; + if (def || ns || named.length) { + const bits = []; + if (def) bits.push(def); + if (ns) bits.push(`* as ${ns}`); + if (named.length) bits.push(`{ ${named.map((el) => el.getText()).join(", ")} }`); + r = `import ${c.isTypeOnly ? "type " : ""}${bits.join(", ")} from ${st.moduleSpecifier.getText()};`; + } + const end = r === "" && out[st.end] === "\n" ? st.end + 1 : st.end; + importEdits.push([st.getStart(), end, r]); +} +let final = ""; +cursor = 0; +for (const [a, b, r] of importEdits.sort((x, y) => x[0] - y[0])) { + final += out.slice(cursor, a) + r; + cursor = b; +} +final += out.slice(cursor); +const lastImport = [ + ...ts.createSourceFile(modulePath, final, ts.ScriptTarget.Latest, true).statements, +] + .filter(ts.isImportDeclaration) + .at(-1); +const add = [...moduleNeeds].map( + ([spec, names]) => `import { ${[...names].sort().join(", ")} } from "${spec}";`, +); +const at = lastImport ? lastImport.end : 0; +final = final.slice(0, at) + (add.length ? "\n" + add.join("\n") : "") + final.slice(at); +writeFileSync(modulePath, final); +// Format what changed with the tools checkout's oxfmt, so the split and +// every rehome of it are formatted the same way. +const touched = [ + modulePath, + ...[...files].filter(([, e]) => e.decls.length).map(([f]) => join(moduleDir, f)), +]; +execFileSync(join(toolsDir, "node_modules/.bin/oxfmt"), touched, { + cwd: repo, + stdio: ["ignore", "ignore", "inherit"], +}); +console.log(`${moved} methods moved; ${publicize.size} members reached through host`); diff --git a/scripts/split/split-module.mjs b/scripts/split/split-module.mjs new file mode 100644 index 000000000..5f7cbf549 --- /dev/null +++ b/scripts/split/split-module.mjs @@ -0,0 +1,539 @@ +#!/usr/bin/env node +/* Split a large TypeScript module into files by a manifest of top-level + * declaration names, as a pure move. + * + * node split-module.mjs plan compute the manifest from seeds + * node split-module.mjs apply extract branches, move declarations + * + * : + * { "tsconfig": "packages/compiler/tsconfig.json", + * "module": "packages/compiler/src/frontend/lowering/lower-exprs.ts", + * "extract": [{ "function": "lowerExprInner", "condition": "ts.isIdentifier(expr)", + * "name": "lowerIdentifierExpr", "exprType": "ts.Identifier" }, ...], + * "seeds": { "expressions/binary.ts": ["lowerBinary", ...], ... }, + * "manifest": "" } + * + * It runs on the checkout in the current directory, or --repo , and + * loads typescript from that checkout or --tools . + * + * extract turns `if () { ...block... }` at the top level of the + * named function into `if () return (lowerer, expr, loc);` + * and adds `function (lowerer: Lowerer, expr: , loc: SrcLoc): + * IrExpr { ...block... }` after it. The block must end in a call that never + * returns, so no path falls through. + * + * plan assigns each seed to its file, then moves every other top-level + * declaration whose referrers all sit in one file (other than the module) + * into that file, to a fixpoint. Module-level `let` bindings stay put (an + * importer cannot assign them). The manifest lists file → names. + * + * apply moves the manifest's declarations with their leading comments, + * in source order, into the new files. Each new file imports what its + * declarations reference: names from the module or another new file, and + * the module's own imports with their paths rebased. The module imports the + * moved names it still uses, re-exports the moved names that were exported, + * and drops imports nothing uses anymore. Moved declarations that something + * outside their file references get `export`. Names a manifest lists but + * the module lacks are skipped, and new declarations stay in the module, + * so the same manifest applies to an edited copy of the module. */ +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { dirname, join, relative, resolve, posix } from "node:path"; +import { createRequire } from "node:module"; +import { execFileSync } from "node:child_process"; + +const [command, configPath, ...flags] = process.argv.slice(2); +const config = JSON.parse(readFileSync(configPath, "utf8")); +const flag = (name) => { + const i = flags.indexOf(name); + return i >= 0 ? flags[i + 1] : undefined; +}; +// --repo runs on another checkout (a temporary worktree); --tools names the +// checkout whose node_modules supply typescript. +const repo = resolve(flag("--repo") ?? config.repo ?? process.cwd()); +// The manifest path is relative to the config file. +config.manifest = resolve(dirname(resolve(configPath)), flag("--manifest") ?? config.manifest); +const toolsDir = resolve(flag("--tools") ?? config.repo ?? process.cwd()); +const require = createRequire(join(toolsDir, "package.json")); +const ts = require("typescript"); +const modulePath = join(repo, config.module); +const moduleDir = dirname(modulePath); + +function parse(text, path = modulePath) { + return ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); +} + +/* ── extract ─────────────────────────────────────────────────────────── */ + +function extractBranches(text) { + for (const spec of config.extract ?? []) { + const sf = parse(text); + const fn = sf.statements.find( + (s) => ts.isFunctionDeclaration(s) && s.name?.text === spec.function, + ); + if (!fn) throw new Error(`no function ${spec.function}`); + const branch = fn.body.statements.find( + (s) => + ts.isIfStatement(s) && + s.expression.getText() === spec.condition && + ts.isBlock(s.thenStatement) && + !s.elseStatement, + ); + if (!branch) { + console.error(`skip: no \`if (${spec.condition}) { ... }\` in ${spec.function}`); + continue; + } + const block = branch.thenStatement; + const last = block.statements.at(-1); + if ( + !last || + !(ts.isExpressionStatement(last) || ts.isReturnStatement(last) || ts.isThrowStatement(last)) + ) + throw new Error( + `${spec.condition}: the block must end in a never-returning call, return or throw`, + ); + // The block's inner text, dedented one level (two spaces). + const inner = text.slice(block.statements[0].getFullStart(), block.statements.at(-1).end); + const body = inner + .split("\n") + .map((line) => (line.startsWith(" ") ? line.slice(2) : line)) + .join("\n") + .replace(/^\n+/, ""); + const call = `if (${spec.condition}) return ${spec.name}(lowerer, expr, loc);`; + const fnText = `\n\nfunction ${spec.name}(lowerer: Lowerer, expr: ${spec.exprType}, loc: SrcLoc): IrExpr {\n ${body.trimStart()}\n}`; + text = + text.slice(0, branch.getStart()) + + call + + text.slice(branch.end, fn.end) + + fnText + + text.slice(fn.end); + } + return text; +} + +/* ── analysis ────────────────────────────────────────────────────────── */ + +function declNames(st) { + if ( + ts.isFunctionDeclaration(st) || + ts.isClassDeclaration(st) || + ts.isInterfaceDeclaration(st) || + ts.isTypeAliasDeclaration(st) || + ts.isEnumDeclaration(st) + ) + return st.name ? [st.name.text] : []; + if (ts.isVariableStatement(st)) + return st.declarationList.declarations.flatMap((d) => + ts.isIdentifier(d.name) ? [d.name.text] : [], + ); + return []; +} +const isExported = (st) => + st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; +const isTypeDecl = (st) => ts.isInterfaceDeclaration(st) || ts.isTypeAliasDeclaration(st); +const isMutable = (st) => + ts.isVariableStatement(st) && !(st.declarationList.flags & ts.NodeFlags.Const); + +function inTypePosition(node) { + for (let n = node; n.parent; n = n.parent) { + if (ts.isTypeNode(n) || ts.isTypeQueryNode?.(n)) return true; + if (ts.isExpressionWithTypeArguments(n) && n.parent && ts.isHeritageClause(n.parent)) + return false; + if (ts.isStatement(n) || (ts.isExpression(n) && !ts.isIdentifier(n))) break; + } + return false; +} + +/** For the module text: its statements with names, its import bindings, and + * each declaration's references (local names and import bindings), each + * marked type-only when every reference sits in a type position. */ +function analyze(text) { + const sf = parse(text); + const host = ts.createCompilerHost({}); + const tsconfig = ts.getParsedCommandLineOfConfigFile( + join(repo, config.tsconfig), + {}, + { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, + ); + const options = { ...tsconfig.options, noEmit: true }; + const realGet = host.getSourceFile; + host.getSourceFile = (fileName, lang, ...rest) => + resolve(fileName) === modulePath ? sf : realGet.call(host, fileName, lang, ...rest); + const program = ts.createProgram([modulePath], options, host); + const checker = program.getTypeChecker(); + const source = program.getSourceFile(modulePath); + + const imports = new Map(); // binding name -> { decl, spec, kind, typeOnly } + const statements = []; + for (const st of source.statements) { + if (ts.isImportDeclaration(st)) { + const clause = st.importClause; + const typeOnlyDecl = clause?.isTypeOnly ?? false; + if (clause?.name) + imports.set(clause.name.text, { + decl: st, + kind: "default", + typeOnly: typeOnlyDecl, + node: clause, + }); + const nb = clause?.namedBindings; + if (nb && ts.isNamespaceImport(nb)) + imports.set(nb.name.text, { + decl: st, + kind: "namespace", + typeOnly: typeOnlyDecl, + node: nb, + }); + if (nb && ts.isNamedImports(nb)) + for (const el of nb.elements) + imports.set(el.name.text, { + decl: st, + kind: "named", + typeOnly: typeOnlyDecl || el.isTypeOnly, + node: el, + imported: (el.propertyName ?? el.name).text, + }); + continue; + } + statements.push({ node: st, names: declNames(st) }); + } + const byNode = new Map(); + for (const s of statements) byNode.set(s.node, s); + const ownerOf = (decl) => { + for (let n = decl; n; n = n.parent) if (byNode.has(n)) return byNode.get(n); + return undefined; + }; + for (const s of statements) { + const refs = new Map(); // name -> { kind: "local"|"import", typeOnly } + const visit = (node) => { + if (ts.isIdentifier(node)) { + const parent = node.parent; + const isDeclName = + parent && + parent.name === node && + (ts.isFunctionDeclaration(parent) || + ts.isVariableDeclaration(parent) || + ts.isInterfaceDeclaration(parent) || + ts.isTypeAliasDeclaration(parent) || + ts.isParameter(parent) || + ts.isPropertyAssignment(parent) || + ts.isPropertySignature(parent) || + ts.isPropertyDeclaration(parent) || + ts.isMethodDeclaration(parent) || + ts.isBindingElement(parent)); + const isPropertyName = + parent && + ((ts.isPropertyAccessExpression(parent) && parent.name === node) || + (ts.isQualifiedName(parent) && parent.right === node)); + if (!isPropertyName) { + let sym = checker.getSymbolAtLocation(node); + if (ts.isShorthandPropertyAssignment(parent)) + sym = checker.getShorthandAssignmentValueSymbol(parent); + const decl = sym?.declarations?.[0]; + if (decl && decl.getSourceFile() === source) { + const typePos = inTypePosition(node); + let target; + if ( + ts.isImportSpecifier(decl) || + ts.isNamespaceImport(decl) || + ts.isImportClause(decl) + ) { + const name = (ts.isImportClause(decl) ? decl.name : decl.name).text; + target = { name, kind: "import" }; + } else { + const owner = ownerOf(decl); + if (owner && owner !== s && !isDeclName) { + const name = owner.names.find((n) => n === node.text) ?? node.text; + if (owner.names.includes(name)) target = { name, kind: "local" }; + } + } + if (target) { + const prev = refs.get(target.name); + refs.set(target.name, { + kind: target.kind, + typeOnly: (prev ? prev.typeOnly : true) && typePos, + }); + } + } + } + } + ts.forEachChild(node, visit); + }; + visit(s.node); + s.refs = refs; + } + return { source, statements, imports }; +} + +/* ── plan ────────────────────────────────────────────────────────────── */ + +function plan() { + const text = extractBranches(readFileSync(modulePath, "utf8")); + const { statements } = analyze(text); + const fileOf = new Map(); + for (const [file, names] of Object.entries(config.seeds)) + for (const name of names) { + if (!statements.some((s) => s.names.includes(name))) + throw new Error(`seed ${name} not found`); + fileOf.set(name, file); + } + const keep = new Set(config.keep ?? []); + const referrers = new Map(); + for (const s of statements) + for (const [name, ref] of s.refs) + if (ref.kind === "local") { + if (!referrers.has(name)) referrers.set(name, new Set()); + for (const n of s.names) referrers.get(name).add(n); + } + for (let changed = true; changed;) { + changed = false; + for (const s of statements) { + if ( + s.names.length === 0 || + s.names.some((n) => fileOf.has(n) || keep.has(n)) || + isMutable(s.node) + ) + continue; + const users = new Set(); + for (const n of s.names) + for (const u of referrers.get(n) ?? []) if (!s.names.includes(u)) users.add(u); + const files = new Set([...users].map((u) => fileOf.get(u) ?? null)); + if (users.size > 0 && files.size === 1 && !files.has(null)) { + const [file] = files; + for (const n of s.names) fileOf.set(n, file); + changed = true; + } + } + } + const manifest = {}; + for (const s of statements) { + const file = s.names.map((n) => fileOf.get(n)).find(Boolean); + if (file) (manifest[file] ??= []).push(...s.names); + } + writeFileSync(config.manifest, JSON.stringify(manifest, null, 2) + "\n"); + for (const [file, names] of Object.entries(manifest)) console.log(`${file}: ${names.length}`); +} + +/* ── apply ───────────────────────────────────────────────────────────── */ + +function rebase(spec, fromDir, toDir) { + if (!spec.startsWith(".")) return spec; + let rel = posix.normalize(relative(toDir, resolve(fromDir, spec)).split("\\").join("/")); + if (!rel.startsWith(".")) rel = "./" + rel; + return rel; +} +const moduleSpecifierTo = (fromDir, file) => { + let rel = relative(fromDir, file).split("\\").join("/").replace(/\.ts$/, ".js"); + return rel.startsWith(".") ? rel : "./" + rel; +}; + +/** The statement's text with its leading comments, without leading blank lines. */ +function fullText(text, st) { + return text.slice(st.getFullStart(), st.end).replace(/^\s*\n/, ""); +} + +function apply() { + const manifest = JSON.parse(readFileSync(config.manifest, "utf8")); + let text = extractBranches(readFileSync(modulePath, "utf8")); + const { source, statements, imports } = analyze(text); + const fileOf = new Map(); + for (const [file, names] of Object.entries(manifest)) for (const n of names) fileOf.set(n, file); + const stFile = (s) => s.names.map((n) => fileOf.get(n)).find(Boolean) ?? null; + const nameFile = new Map(); + const nameStmt = new Map(); + for (const s of statements) + for (const n of s.names) { + nameFile.set(n, stFile(s)); + nameStmt.set(n, s); + } + + // Which names need `export`: referenced from another file, or exported already. + const needsExport = new Set(); + for (const s of statements) + for (const [name, ref] of s.refs) + if (ref.kind === "local" && nameFile.get(name) !== stFile(s)) needsExport.add(name); + + const files = new Map(); // file -> { decls: [], imports: Map } + const fileImports = (file) => { + if (!files.has(file)) files.set(file, { decls: [], named: new Map(), other: [] }); + return files.get(file); + }; + const addNamed = (entry, spec, name, imported, typeOnly) => { + if (!entry.named.has(spec)) entry.named.set(spec, new Map()); + const m = entry.named.get(spec); + const prev = m.get(name); + m.set(name, { imported, typeOnly: prev ? prev.typeOnly && typeOnly : typeOnly }); + }; + const moduleEntry = { decls: [], named: new Map(), other: [] }; + for (const s of statements) { + const file = stFile(s); + const entry = file ? fileImports(file) : moduleEntry; + const dir = file ? dirname(join(moduleDir, file)) : moduleDir; + if (file) { + let t = fullText(text, s.node); + if (!isExported(s.node) && s.names.some((n) => needsExport.has(n))) { + const start = s.node.getStart() - s.node.getFullStart(); + const lead = text.slice(s.node.getFullStart(), s.node.getStart()).replace(/^\s*\n/, ""); + t = lead + "export " + text.slice(s.node.getStart(), s.node.end); + void start; + } + entry.decls.push(t); + } + for (const [name, ref] of s.refs) { + if (ref.kind === "local") { + const target = nameFile.get(name) ?? null; + if (target === file) continue; + const targetPath = target ? join(moduleDir, target) : modulePath; + const typeOnly = ref.typeOnly || isTypeDecl(nameStmt.get(name).node); + addNamed(entry, moduleSpecifierTo(dir, targetPath), name, name, typeOnly); + } else if (file) { + const imp = imports.get(name); + const spec = rebase(imp.decl.moduleSpecifier.text, moduleDir, dir); + if (imp.kind === "named") addNamed(entry, spec, name, imp.imported, imp.typeOnly); + else entry.other.push({ spec, kind: imp.kind, name, typeOnly: imp.typeOnly }); + } + } + } + + const renderImports = (entry) => { + const lines = []; + const seen = new Set(); + for (const o of entry.other) { + const key = `${o.kind}:${o.name}:${o.spec}`; + if (seen.has(key)) continue; + seen.add(key); + lines.push( + o.kind === "namespace" + ? `import ${o.typeOnly ? "type " : ""}* as ${o.name} from "${o.spec}";` + : `import ${o.typeOnly ? "type " : ""}${o.name} from "${o.spec}";`, + ); + } + for (const [spec, names] of entry.named) { + const all = [...names.entries()]; + const allType = all.every(([, v]) => v.typeOnly); + const parts = all + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map( + ([name, v]) => + `${!allType && v.typeOnly ? "type " : ""}${v.imported === name ? name : `${v.imported} as ${name}`}`, + ); + lines.push(`import ${allType ? "type " : ""}{ ${parts.join(", ")} } from "${spec}";`); + } + return lines; + }; + + // New files. + for (const [file, entry] of files) { + const header = config.headers?.[file]; + const out = [ + ...(header ? [header] : []), + ...renderImports(entry), + "", + entry.decls.join("\n\n"), + "", + ].join("\n"); + const path = join(moduleDir, file); + mkdirSync(dirname(path), { recursive: true }); + if (existsSync(path)) throw new Error(`${file} exists`); + writeFileSync(path, out); + } + + // The module: drop moved declarations, rewrite its import block. + const keptRefs = new Set(); + for (const s of statements) + if (!stFile(s)) for (const [name, ref] of s.refs) if (ref.kind === "import") keptRefs.add(name); + const pieces = []; + let cursor = 0; + const removals = []; + for (const s of statements) if (stFile(s)) removals.push([s.node.getFullStart(), s.node.end]); + // Import declarations: rebuild each without bindings nothing uses. + const importEdits = []; + for (const st of source.statements) { + if (!ts.isImportDeclaration(st)) continue; + const clause = st.importClause; + if (!clause) continue; // side-effect import + const used = (n) => keptRefs.has(n); + const def = clause.name && used(clause.name.text) ? clause.name.text : null; + const nb = clause.namedBindings; + let ns = null; + let named = []; + if (nb && ts.isNamespaceImport(nb) && used(nb.name.text)) ns = nb.name.text; + if (nb && ts.isNamedImports(nb)) named = nb.elements.filter((el) => used(el.name.text)); + const keepAll = + def === (clause.name?.text ?? null) && + (!nb || (ts.isNamespaceImport(nb) ? ns !== null : named.length === nb.elements.length)); + if (keepAll) continue; + let replacement = ""; + if (def || ns || named.length) { + const bits = []; + if (def) bits.push(def); + if (ns) bits.push(`* as ${ns}`); + if (named.length) bits.push(`{ ${named.map((el) => el.getText()).join(", ")} }`); + replacement = `import ${clause.isTypeOnly ? "type " : ""}${bits.join(", ")} from ${st.moduleSpecifier.getText()};`; + } + // A removed declaration takes its line break with it. + const end = replacement === "" && text[st.end] === "\n" ? st.end + 1 : st.end; + importEdits.push([st.getStart(), end, replacement]); + } + // Declarations that stay but that a moved one references get `export`. + const exportEdits = []; + for (const s of statements) + if (!stFile(s) && !isExported(s.node) && s.names.some((n) => needsExport.has(n))) + exportEdits.push([s.node.getStart(), s.node.getStart(), "export "]); + const edits = [...removals.map(([a, b]) => [a, b, ""]), ...importEdits, ...exportEdits].sort( + (x, y) => x[0] - y[0], + ); + for (const [a, b, r] of edits) { + pieces.push(text.slice(cursor, a), r); + cursor = b; + } + pieces.push(text.slice(cursor)); + let moduleText = pieces.join(""); + // Imports of moved names the module still uses, and re-exports of moved + // names that were exported (other modules import them from here). + const addLines = renderImports(moduleEntry); + const reexports = new Map(); + for (const s of statements) { + const file = stFile(s); + if (!file || !isExported(s.node)) continue; + const spec = moduleSpecifierTo(moduleDir, join(moduleDir, file)); + if (!reexports.has(spec)) reexports.set(spec, []); + for (const n of s.names) reexports.get(spec).push({ name: n, type: isTypeDecl(s.node) }); + } + for (const [spec, names] of reexports) { + const values = names + .filter((n) => !n.type) + .map((n) => n.name) + .sort(); + const types = names + .filter((n) => n.type) + .map((n) => n.name) + .sort(); + if (values.length) addLines.push(`export { ${values.join(", ")} } from "${spec}";`); + if (types.length) addLines.push(`export type { ${types.join(", ")} } from "${spec}";`); + } + // Insert after the last import declaration. + const reparsed = parse(moduleText); + const lastImport = [...reparsed.statements].filter(ts.isImportDeclaration).at(-1); + const at = lastImport ? lastImport.end : 0; + moduleText = + moduleText.slice(0, at) + + (addLines.length ? "\n" + addLines.join("\n") : "") + + moduleText.slice(at); + moduleText = moduleText.replace(/\n{3,}/g, "\n\n"); + writeFileSync(modulePath, moduleText); + // Format what changed with the tools checkout's oxfmt, so the split and + // every rehome of it are formatted the same way. + const touched = [modulePath, ...[...files.keys()].map((f) => join(moduleDir, f))]; + execFileSync(join(toolsDir, "node_modules/.bin/oxfmt"), touched, { + cwd: repo, + stdio: ["ignore", "ignore", "inherit"], + }); + for (const [file, entry] of files) console.log(`${file}: ${entry.decls.length} declarations`); +} + +if (command === "plan") plan(); +else if (command === "apply") apply(); +else { + console.error("usage: split-module.mjs plan|apply "); + process.exit(2); +} From 71e3dc5b80fc1af370961605b636edf3b76c235f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 03:21:11 -0500 Subject: [PATCH 002/169] Keep shorthand-property imports in split methods and treat extends as a value use in split modules --- scripts/split/split-methods.mjs | 5 +- scripts/split/split-module.mjs | 6 +- tests/harness/split-tools.test.ts | 121 ++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/harness/split-tools.test.ts diff --git a/scripts/split/split-methods.mjs b/scripts/split/split-methods.mjs index 66d6b36ad..0b4f9a161 100644 --- a/scripts/split/split-methods.mjs +++ b/scripts/split/split-methods.mjs @@ -351,7 +351,10 @@ const walk = (n) => { !(n.parent && ts.isImportClause(n.parent)) && !(n.parent && ts.isNamespaceImport(n.parent)) ) { - const sym = checker2.getSymbolAtLocation(n); + const sym = + n.parent && ts.isShorthandPropertyAssignment(n.parent) + ? checker2.getShorthandAssignmentValueSymbol(n.parent) + : checker2.getSymbolAtLocation(n); const d = sym?.declarations?.[0]; if (d && (ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d))) used.add(d.name.text); diff --git a/scripts/split/split-module.mjs b/scripts/split/split-module.mjs index 5f7cbf549..aa20baaf4 100644 --- a/scripts/split/split-module.mjs +++ b/scripts/split/split-module.mjs @@ -135,9 +135,11 @@ const isMutable = (st) => function inTypePosition(node) { for (let n = node; n.parent; n = n.parent) { + // A class's `extends` names a value; `implements` and an interface's + // `extends` name types. + if (ts.isExpressionWithTypeArguments(n) && ts.isHeritageClause(n.parent)) + return !(n.parent.token === ts.SyntaxKind.ExtendsKeyword && ts.isClassLike(n.parent.parent)); if (ts.isTypeNode(n) || ts.isTypeQueryNode?.(n)) return true; - if (ts.isExpressionWithTypeArguments(n) && n.parent && ts.isHeritageClause(n.parent)) - return false; if (ts.isStatement(n) || (ts.isExpression(n) && !ts.isIdentifier(n))) break; } return false; diff --git a/tests/harness/split-tools.test.ts b/tests/harness/split-tools.test.ts new file mode 100644 index 000000000..659015cc6 --- /dev/null +++ b/tests/harness/split-tools.test.ts @@ -0,0 +1,121 @@ +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; +import { afterEach, expect, test } from "vitest"; + +// The repository root supplies typescript and oxfmt to the split tools. +const root = fileURLToPath(new URL("../..", import.meta.url)); +const tool = (name: string): string => join(root, "scripts/split", name); +const dirs: string[] = []; + +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +const compilerOptions = { + strict: true, + noEmit: true, + target: "ES2022", + module: "ESNext", + moduleResolution: "Bundler", +}; + +/** A checkout holding `files` (paths relative to its root) and a tsconfig. */ +function checkout(files: Record): string { + const dir = mkdtempSync(join(tmpdir(), "split-tools-")); + dirs.push(dir); + const all = { ...files, "tsconfig.json": JSON.stringify({ compilerOptions }) }; + for (const [path, text] of Object.entries(all)) { + mkdirSync(dirname(join(dir, path)), { recursive: true }); + writeFileSync(join(dir, path), text); + } + return dir; +} + +function run(script: string, args: string[], repo: string): void { + execFileSync(process.execPath, [tool(script), ...args, "--repo", repo, "--tools", root], { + cwd: repo, + stdio: "pipe", + }); +} + +/** Type errors in the checkout's files. */ +function diagnostics(dir: string): string[] { + const parsed = ts.getParsedCommandLineOfConfigFile( + join(dir, "tsconfig.json"), + {}, + { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: () => {}, + }, + ); + const program = ts.createProgram(parsed!.fileNames, parsed!.options); + return ts + .getPreEmitDiagnostics(program) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n")); +} + +test("split-methods keeps an import a staying method uses as a shorthand property", () => { + const dir = checkout({ + "src/helper.ts": "export const helper = 2;\n", + "src/host.ts": [ + 'import { helper } from "./helper.js";', + "", + "export class Host {", + " value = 1;", + " keep(): { helper: number } {", + " return { helper };", + " }", + " moved(): number {", + " return this.value + 1;", + " }", + "}", + "", + ].join("\n"), + "split.json": JSON.stringify({ + module: "src/host.ts", + class: "Host", + methods: { "moved.ts": ["moved"] }, + }), + }); + run("split-methods.mjs", ["apply", join(dir, "split.json")], dir); + expect(readFileSync(join(dir, "src/host.ts"), "utf8")).toContain( + 'import { helper } from "./helper.js";', + ); + expect(diagnostics(dir)).toEqual([]); +}); + +test("split-module imports a moved class's local base class as a value", () => { + const dir = checkout({ + "src/shapes.ts": [ + "export class Base {", + " x = 1;", + "}", + "", + "export class Derived extends Base {", + " y = 2;", + "}", + "", + "export function base(): Base {", + " return new Base();", + "}", + "", + ].join("\n"), + "split.json": JSON.stringify({ + tsconfig: "tsconfig.json", + module: "src/shapes.ts", + seeds: { "derived.ts": ["Derived"] }, + manifest: "manifest.json", + }), + }); + const config = join(dir, "split.json"); + run("split-module.mjs", ["plan", config], dir); + run("split-module.mjs", ["apply", config], dir); + expect(readFileSync(join(dir, "src/derived.ts"), "utf8")).toContain( + 'import { Base } from "./shapes.js";', + ); + expect(diagnostics(dir)).toEqual([]); +}); From d244b84931d4f706b31f81c90b6cac8fbb61ef02 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 03:47:21 -0500 Subject: [PATCH 003/169] Keep the split scripts' reads and writes inside the worktree --- scripts/split/README.md | 2 +- scripts/split/confine.mjs | 89 +++++++++++++++++ scripts/split/move-state.mjs | 29 +++--- scripts/split/rehome-split.mjs | 21 ++-- scripts/split/split-methods.mjs | 17 ++-- scripts/split/split-module.mjs | 31 +++--- .../split-tools/symlink/outside/linked.ts | 2 + .../split-tools/symlink/repo/src/host.ts | 6 ++ .../split-tools/symlink/repo/src/linked.ts | 1 + tests/harness/split-tools.test.ts | 98 ++++++++++++++++++- 10 files changed, 255 insertions(+), 41 deletions(-) create mode 100644 scripts/split/confine.mjs create mode 100644 tests/fixtures/split-tools/symlink/outside/linked.ts create mode 100644 tests/fixtures/split-tools/symlink/repo/src/host.ts create mode 120000 tests/fixtures/split-tools/symlink/repo/src/linked.ts diff --git a/scripts/split/README.md b/scripts/split/README.md index 7d79b35c1..e3cdfd077 100644 --- a/scripts/split/README.md +++ b/scripts/split/README.md @@ -9,7 +9,7 @@ Run them from a checkout with `node_modules` installed. That checkout is the too - `split-methods.mjs apply ` moves class methods into exported functions that take the instance as a parameter (`emitter`). The class keeps a one-line stub with the same signature, so callers don't change. - `move-state.mjs apply ` moves a group of class fields into a feature's state object, defined in the feature's file with `defineModuleState` (`packages/compiler/src/backend/llvm/emitter-state.ts`). It rewrites `this.` and `emitter.` to `KEY.of(this).` and creates files from templates. It also applies the change's other edits as literal replacements, in order. A replacement whose text isn't found the expected number of times is reported and skipped. -Each transform takes `--repo ` (the tree to rewrite) and `--tools `, and formats what it writes with oxfmt. `configs/` holds the configs of splits that have landed. For example, `configs/loop-state/` moves the loop optimizations' state out of `LlEmitter`. +Each transform takes `--repo ` (the tree to rewrite) and `--tools `, and formats what it writes with oxfmt. The tools read and write the tree's files only at real paths inside `--repo`, and a config's templates and manifest only inside the config's directory. A path that resolves elsewhere, through a symlink or `..`, stops the tool with an error (`confine.mjs`). `configs/` holds the configs of splits that have landed. For example, `configs/loop-state/` moves the loop optimizations' state out of `LlEmitter`. ## Rehoming an open branch ```sh diff --git a/scripts/split/confine.mjs b/scripts/split/confine.mjs new file mode 100644 index 000000000..14af8b85a --- /dev/null +++ b/scripts/split/confine.mjs @@ -0,0 +1,89 @@ +/* Path confinement for the split tools. A branch they rewrite can commit + * symlinks, so every file a tool reads or writes goes through a guard for + * the directory it belongs to (the checkout being rewritten, or the + * config's directory). The guard resolves the path's real location, through + * every symlink on the way, and throws when that location is outside the + * directory's real path. A path that doesn't exist yet resolves through its + * nearest existing parent, so a write can't create a file through a + * symlinked directory or a dangling symlink either. */ +import { + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + readlinkSync, + realpathSync, + writeFileSync, +} from "node:fs"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; + +const isInside = (root, path) => { + const rel = relative(root, path); + return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)); +}; + +/** The real location of `path`, which need not exist: the real path of its + * nearest existing entry joined with the rest. A dangling symlink resolves + * to where its target would be. */ +function realLocation(path, hops = 0) { + const rest = []; + for (let at = resolve(path); ; at = dirname(at)) { + let stat = null; + try { + stat = lstatSync(at); + } catch (e) { + if (e.code !== "ENOENT" && e.code !== "ENOTDIR") throw e; + } + if (stat) { + try { + return join(realpathSync(at), ...rest); + } catch (e) { + if (e.code !== "ENOENT" || !stat.isSymbolicLink() || hops > 40) throw e; + return realLocation(join(resolve(dirname(at), readlinkSync(at)), ...rest), hops + 1); + } + } + if (dirname(at) === at) return resolve(path); + rest.unshift(basename(at)); + } +} + +/** A guard for the files under `root`. */ +export function confine(root) { + const lexical = resolve(root); + const real = realpathSync(lexical); + const check = (path) => { + const location = realLocation(path); + if (!isInside(real, location)) + throw new Error(`refusing ${path}: it resolves to ${location}, outside ${real}`); + return location; + }; + return { + /** The path's real location; throws when it is outside the root. */ + check, + /** True when `path` names a file under the root (before symlinks). */ + contains: (path) => isInside(lexical, resolve(path)) || isInside(real, resolve(path)), + read: (path) => readFileSync(check(path), "utf8"), + write: (path, text) => writeFileSync(check(path), text), + exists: (path) => existsSync(check(path)), + mkdir: (path) => mkdirSync(check(path), { recursive: true }), + list: (path) => readdirSync(check(path)), + }; +} + +/** Routes a TypeScript host's (or `ts.sys`-like object's) file reads under + * the guard's root through the guard; other files (the TypeScript libraries) + * read as before. */ +export function confineHost(host, guard) { + const checked = + (read) => + (fileName, ...rest) => { + if (guard.contains(fileName)) guard.check(fileName); + return read.call(host, fileName, ...rest); + }; + return { + ...host, + ...(host.getSourceFile ? { getSourceFile: checked(host.getSourceFile) } : {}), + readFile: checked(host.readFile), + }; +} diff --git a/scripts/split/move-state.mjs b/scripts/split/move-state.mjs index 5da787ac2..b70023c67 100644 --- a/scripts/split/move-state.mjs +++ b/scripts/split/move-state.mjs @@ -23,10 +23,11 @@ * rewritten file imports the key. * The group's file must already exist and hold everything else (header, * setup function); the tool appends the interface and key to it. */ -import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; +import { confine } from "./confine.mjs"; const [command, configPath, ...flags] = process.argv.slice(2); if (command !== "apply") throw new Error("usage: move-state.mjs apply "); @@ -41,6 +42,9 @@ const require = createRequire(join(toolsDir, "package.json")); const ts = require("typescript"); const modulePath = join(repo, config.module); const dir = join(repo, config.dir); +// Every read and write stays inside the checkout, or for the templates, the +// config's directory (confine.mjs). +const checkout = confine(repo); // Adds an import line after the file's first block of imports (up to a // blank line or other code). @@ -56,6 +60,7 @@ const addImport = (text, line) => { return text.slice(0, at) + line + "\n" + text.slice(at); }; const configDir = dirname(resolve(configPath)); +const configFiles = confine(configDir); // The names the class's instance goes by in the directory's sources. const receivers = config.receivers ?? ["this", "emitter"]; const touched = new Set(); @@ -63,11 +68,11 @@ const touched = new Set(); // branch already has the file). for (const [file, template] of Object.entries(config.create ?? {})) { const path = join(dir, file); - if (existsSync(path)) continue; - writeFileSync(path, readFileSync(join(configDir, template), "utf8")); + if (checkout.exists(path)) continue; + checkout.write(path, configFiles.read(join(configDir, template))); touched.add(path); } -let moduleText = readFileSync(modulePath, "utf8"); +let moduleText = checkout.read(modulePath); for (const group of config.groups) { const sf = ts.createSourceFile(modulePath, moduleText, ts.ScriptTarget.Latest, true); const cls = sf.statements.find((s) => ts.isClassDeclaration(s) && s.name?.text === config.class); @@ -124,22 +129,22 @@ for (const group of config.groups) { `export interface ${group.interfaceName ?? iName} {\n${iface.join("\n")}\n}\n\n` + `export const ${group.key} = defineModuleState<${group.interfaceName ?? iName}>(() => ({\n${factory.join("\n")}\n}));\n`; const groupPath = join(dir, group.file); - let groupText = readFileSync(groupPath, "utf8"); + let groupText = checkout.read(groupPath); if (!groupText.includes('from "./emitter-state.js"')) groupText = addImport(groupText, `import { defineModuleState } from "./emitter-state.js";`); groupText = groupText.trimEnd() + "\n" + stateText; - writeFileSync(groupPath, groupText); + checkout.write(groupPath, groupText); touched.add(groupPath); group.rename = group.present.length ? new RegExp(`(? f.endsWith(".ts"))) { +for (const file of checkout.list(dir).filter((f) => f.endsWith(".ts"))) { const path = join(dir, file); - let text = path === modulePath ? moduleText : readFileSync(path, "utf8"); + let text = path === modulePath ? moduleText : checkout.read(path); let changed = false; for (const group of config.groups) { if (!group.rename) continue; @@ -171,7 +176,7 @@ for (const file of readdirSync(dir).filter((f) => f.endsWith(".ts"))) { } } if (changed) { - writeFileSync(path, text); + checkout.write(path, text); touched.add(path); } } @@ -182,7 +187,7 @@ for (const file of readdirSync(dir).filter((f) => f.endsWith(".ts"))) { let skipped = 0; for (const r of config.replacements ?? []) { const path = join(dir, r.file); - const text = existsSync(path) ? readFileSync(path, "utf8") : ""; + const text = checkout.exists(path) ? checkout.read(path) : ""; const count = text.split(r.find).length - 1; if (count !== (r.count ?? 1)) { console.warn( @@ -191,7 +196,7 @@ for (const r of config.replacements ?? []) { skipped++; continue; } - writeFileSync(path, text.split(r.find).join(r.replace)); + checkout.write(path, text.split(r.find).join(r.replace)); touched.add(path); } const oxfmt = join(toolsDir, "node_modules/.bin/oxfmt"); diff --git a/scripts/split/rehome-split.mjs b/scripts/split/rehome-split.mjs index 2884558fb..89c32d6b7 100644 --- a/scripts/split/rehome-split.mjs +++ b/scripts/split/rehome-split.mjs @@ -24,10 +24,11 @@ * instead. When the cherry-pick conflicts, the script stops and * leaves the work tree with the conflicts to resolve; see the message. */ import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { confine } from "./confine.mjs"; const args = process.argv.slice(2); const opt = (name, fallback) => { @@ -157,7 +158,9 @@ try { : Object.keys( (({ files, ...rest }) => files ?? rest)( JSON.parse( - readFileSync(resolve(dirname(resolve(configPath)), config.manifest), "utf8"), + confine(dirname(resolve(configPath))).read( + resolve(dirname(resolve(configPath)), config.manifest), + ), ), ), ); @@ -166,7 +169,10 @@ try { for (const file of gitIn(resultDir, "diff", "--name-only", "--diff-filter=U") .split("\n") .filter(Boolean)) { - if (splitFiles.has(file) && resolveImportConflicts(join(resultDir, file))) { + if ( + splitFiles.has(file) && + resolveImportConflicts(confine(resultDir), join(resultDir, file)) + ) { execFileSync(join(tools, "node_modules/.bin/oxfmt"), [file], { cwd: resultDir, stdio: ["ignore", "ignore", "inherit"], @@ -212,11 +218,12 @@ try { /* Rewrites every conflict in `path` whose two sides hold only import * declarations as the union of both: one declaration per module and kind, - * with the named imports of both sides in first-seen order. Returns false, + * with the named imports of both sides in first-seen order, read and + * written through the result checkout's guard. Returns false, * leaving the file unchanged, when any conflict holds other code. */ -function resolveImportConflicts(path) { +function resolveImportConflicts(checkout, path) { const ts = createRequire(join(tools, "package.json"))("typescript"); - const text = readFileSync(path, "utf8"); + const text = checkout.read(path); const region = /^<<<<<<< [^\n]*\n([\s\S]*?)^=======\n([\s\S]*?)^>>>>>>> [^\n]*\n/gm; const imports = (side) => { const sf = ts.createSourceFile("side.ts", side, ts.ScriptTarget.Latest, true); @@ -266,7 +273,7 @@ function resolveImportConflicts(path) { ); }); if (!ok || resolved === text) return false; - writeFileSync(path, pruneUnusedImports(ts, resolved, imported)); + checkout.write(path, pruneUnusedImports(ts, resolved, imported)); return true; } diff --git a/scripts/split/split-methods.mjs b/scripts/split/split-methods.mjs index 0b4f9a161..05d72c4be 100644 --- a/scripts/split/split-methods.mjs +++ b/scripts/split/split-methods.mjs @@ -20,10 +20,11 @@ * reference from the module's scope, with paths rebased; the module imports * the functions and drops imports nothing uses anymore. Names the config * lists but the class lacks are skipped. */ -import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { dirname, join, relative, resolve, posix } from "node:path"; import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; +import { confine, confineHost } from "./confine.mjs"; const [command, configPath, ...flags] = process.argv.slice(2); if (command !== "apply") throw new Error("usage: split-methods.mjs apply "); @@ -33,6 +34,8 @@ const flag = (name) => { return i >= 0 ? flags[i + 1] : undefined; }; const repo = resolve(flag("--repo") ?? process.cwd()); +// Every read and write in the checkout stays inside it (confine.mjs). +const checkout = confine(repo); const toolsDir = resolve(flag("--tools") ?? process.cwd()); const require = createRequire(join(toolsDir, "package.json")); const ts = require("typescript"); @@ -42,8 +45,8 @@ const className = config.class; // The host parameter's name; a moved body that already uses it is refused. const hostName = config.hostName ?? "emitter"; -const text = readFileSync(modulePath, "utf8"); -const host = ts.createCompilerHost({ noEmit: true }); +const text = checkout.read(modulePath); +const host = confineHost(ts.createCompilerHost({ noEmit: true }), checkout); const sf = ts.createSourceFile(modulePath, text, ts.ScriptTarget.Latest, true); const realGet = host.getSourceFile; host.getSourceFile = (f, l, ...r) => @@ -294,9 +297,9 @@ const renderImports = (entry) => { for (const [file, entry] of files) { if (!entry.decls.length) continue; const path = join(moduleDir, file); - if (existsSync(path)) throw new Error(`${file} exists`); + if (checkout.exists(path)) throw new Error(`${file} exists`); const header = config.headers?.[file]; - writeFileSync( + checkout.write( path, [...(header ? [header] : []), ...renderImports(entry), "", entry.decls.join("\n\n"), ""].join( "\n", @@ -336,7 +339,7 @@ out += text.slice(cursor); // Re-analyze the rewritten module for unused imports, then add the new ones. const sf2 = ts.createSourceFile(modulePath, out, ts.ScriptTarget.Latest, true); -const host2 = ts.createCompilerHost({ noEmit: true }); +const host2 = confineHost(ts.createCompilerHost({ noEmit: true }), checkout); const get2 = host2.getSourceFile; host2.getSourceFile = (f, l, ...r) => resolve(f) === modulePath ? sf2 : get2.call(host2, f, l, ...r); @@ -403,7 +406,7 @@ const add = [...moduleNeeds].map( ); const at = lastImport ? lastImport.end : 0; final = final.slice(0, at) + (add.length ? "\n" + add.join("\n") : "") + final.slice(at); -writeFileSync(modulePath, final); +checkout.write(modulePath, final); // Format what changed with the tools checkout's oxfmt, so the split and // every rehome of it are formatted the same way. const touched = [ diff --git a/scripts/split/split-module.mjs b/scripts/split/split-module.mjs index aa20baaf4..ba340df40 100644 --- a/scripts/split/split-module.mjs +++ b/scripts/split/split-module.mjs @@ -36,10 +36,11 @@ * outside their file references get `export`. Names a manifest lists but * the module lacks are skipped, and new declarations stay in the module, * so the same manifest applies to an edited copy of the module. */ -import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { dirname, join, relative, resolve, posix } from "node:path"; import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; +import { confine, confineHost } from "./confine.mjs"; const [command, configPath, ...flags] = process.argv.slice(2); const config = JSON.parse(readFileSync(configPath, "utf8")); @@ -50,6 +51,10 @@ const flag = (name) => { // --repo runs on another checkout (a temporary worktree); --tools names the // checkout whose node_modules supply typescript. const repo = resolve(flag("--repo") ?? config.repo ?? process.cwd()); +// Every read and write stays inside the checkout, or for the manifest, the +// config's directory (confine.mjs). +const checkout = confine(repo); +const configFiles = confine(dirname(resolve(configPath))); // The manifest path is relative to the config file. config.manifest = resolve(dirname(resolve(configPath)), flag("--manifest") ?? config.manifest); const toolsDir = resolve(flag("--tools") ?? config.repo ?? process.cwd()); @@ -150,11 +155,13 @@ function inTypePosition(node) { * marked type-only when every reference sits in a type position. */ function analyze(text) { const sf = parse(text); - const host = ts.createCompilerHost({}); + const host = confineHost(ts.createCompilerHost({}), checkout); + const tsconfigPath = join(repo, config.tsconfig); + checkout.check(tsconfigPath); const tsconfig = ts.getParsedCommandLineOfConfigFile( - join(repo, config.tsconfig), + tsconfigPath, {}, - { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, + { ...confineHost(ts.sys, checkout), onUnRecoverableConfigFileDiagnostic: () => {} }, ); const options = { ...tsconfig.options, noEmit: true }; const realGet = host.getSourceFile; @@ -269,7 +276,7 @@ function analyze(text) { /* ── plan ────────────────────────────────────────────────────────────── */ function plan() { - const text = extractBranches(readFileSync(modulePath, "utf8")); + const text = extractBranches(checkout.read(modulePath)); const { statements } = analyze(text); const fileOf = new Map(); for (const [file, names] of Object.entries(config.seeds)) @@ -311,7 +318,7 @@ function plan() { const file = s.names.map((n) => fileOf.get(n)).find(Boolean); if (file) (manifest[file] ??= []).push(...s.names); } - writeFileSync(config.manifest, JSON.stringify(manifest, null, 2) + "\n"); + configFiles.write(config.manifest, JSON.stringify(manifest, null, 2) + "\n"); for (const [file, names] of Object.entries(manifest)) console.log(`${file}: ${names.length}`); } @@ -334,8 +341,8 @@ function fullText(text, st) { } function apply() { - const manifest = JSON.parse(readFileSync(config.manifest, "utf8")); - let text = extractBranches(readFileSync(modulePath, "utf8")); + const manifest = JSON.parse(configFiles.read(config.manifest)); + let text = extractBranches(checkout.read(modulePath)); const { source, statements, imports } = analyze(text); const fileOf = new Map(); for (const [file, names] of Object.entries(manifest)) for (const n of names) fileOf.set(n, file); @@ -434,9 +441,9 @@ function apply() { "", ].join("\n"); const path = join(moduleDir, file); - mkdirSync(dirname(path), { recursive: true }); - if (existsSync(path)) throw new Error(`${file} exists`); - writeFileSync(path, out); + checkout.mkdir(dirname(path)); + if (checkout.exists(path)) throw new Error(`${file} exists`); + checkout.write(path, out); } // The module: drop moved declarations, rewrite its import block. @@ -522,7 +529,7 @@ function apply() { (addLines.length ? "\n" + addLines.join("\n") : "") + moduleText.slice(at); moduleText = moduleText.replace(/\n{3,}/g, "\n\n"); - writeFileSync(modulePath, moduleText); + checkout.write(modulePath, moduleText); // Format what changed with the tools checkout's oxfmt, so the split and // every rehome of it are formatted the same way. const touched = [modulePath, ...[...files.keys()].map((f) => join(moduleDir, f))]; diff --git a/tests/fixtures/split-tools/symlink/outside/linked.ts b/tests/fixtures/split-tools/symlink/outside/linked.ts new file mode 100644 index 000000000..7e551bf2e --- /dev/null +++ b/tests/fixtures/split-tools/symlink/outside/linked.ts @@ -0,0 +1,2 @@ +// A file outside the checkout that the split tools must not read or write. +export const outside = 1; diff --git a/tests/fixtures/split-tools/symlink/repo/src/host.ts b/tests/fixtures/split-tools/symlink/repo/src/host.ts new file mode 100644 index 000000000..4bf39c941 --- /dev/null +++ b/tests/fixtures/split-tools/symlink/repo/src/host.ts @@ -0,0 +1,6 @@ +export class Host { + count = 0; + moved(): number { + return this.count + 1; + } +} diff --git a/tests/fixtures/split-tools/symlink/repo/src/linked.ts b/tests/fixtures/split-tools/symlink/repo/src/linked.ts new file mode 120000 index 000000000..3a9e39fbe --- /dev/null +++ b/tests/fixtures/split-tools/symlink/repo/src/linked.ts @@ -0,0 +1 @@ +../../outside/linked.ts \ No newline at end of file diff --git a/tests/harness/split-tools.test.ts b/tests/harness/split-tools.test.ts index 659015cc6..778ef152b 100644 --- a/tests/harness/split-tools.test.ts +++ b/tests/harness/split-tools.test.ts @@ -1,5 +1,14 @@ -import { execFileSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -119,3 +128,88 @@ test("split-module imports a moved class's local base class as a value", () => { ); expect(diagnostics(dir)).toEqual([]); }); + +/* A copy of tests/fixtures/split-tools/symlink: `repo/` is the checkout the + * tools rewrite, and its committed `src/linked.ts` links to `outside/linked.ts` + * next to it. */ +const symlinkFixture = fileURLToPath(new URL("../fixtures/split-tools/symlink", import.meta.url)); +const outsideText = readFileSync(join(symlinkFixture, "outside/linked.ts"), "utf8"); + +function symlinkCheckout(): { dir: string; repo: string } { + const dir = mkdtempSync(join(tmpdir(), "split-tools-")); + dirs.push(dir); + cpSync(symlinkFixture, dir, { recursive: true, verbatimSymlinks: true }); + return { dir, repo: join(dir, "repo") }; +} + +/** Runs a tool that must refuse; answers its stderr. */ +function refused(script: string, config: object, dir: string, repo: string): string { + const path = join(dir, "split.json"); + writeFileSync(path, JSON.stringify(config)); + const result = spawnSync( + process.execPath, + [tool(script), "apply", path, "--repo", repo, "--tools", root], + { cwd: repo, encoding: "utf8" }, + ); + expect(result.status).not.toBe(0); + return result.stderr; +} + +test("split-methods refuses to write a new file through a symlink that leaves the checkout", () => { + const { dir, repo } = symlinkCheckout(); + // A link to a file that doesn't exist yet: writing through it would create the file. + symlinkSync("../../outside/created.ts", join(repo, "src/moved.ts")); + const stderr = refused( + "split-methods.mjs", + { module: "src/host.ts", class: "Host", methods: { "moved.ts": ["moved"] } }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); + expect(existsSync(join(dir, "outside/created.ts"))).toBe(false); +}); + +test("split-methods refuses a module that links outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + const stderr = refused( + "split-methods.mjs", + { module: "src/linked.ts", class: "Host", methods: { "moved.ts": ["moved"] } }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); +}); + +test("split-module refuses a module that links outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + writeFileSync(join(repo, "tsconfig.json"), JSON.stringify({ compilerOptions })); + writeFileSync(join(dir, "manifest.json"), JSON.stringify({ "moved.ts": ["outside"] })); + const stderr = refused( + "split-module.mjs", + { tsconfig: "tsconfig.json", module: "src/linked.ts", manifest: "manifest.json" }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); +}); + +test("move-state refuses to rewrite a file that links outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + writeFileSync(join(repo, "src/host-state.ts"), ""); + const stderr = refused( + "move-state.mjs", + { + module: "src/host.ts", + class: "Host", + dir: "src", + groups: [ + { file: "host-state.ts", key: "COUNTS", interfaceName: "Counts", fields: ["count"] }, + ], + replacements: [{ file: "linked.ts", find: "export const outside = 1;", replace: "" }], + }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); + expect(readFileSync(join(dir, "outside/linked.ts"), "utf8")).toBe(outsideText); +}); From 054cb50ea4381566b4438a0e5b44448b426e4c04 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:31:20 -0500 Subject: [PATCH 004/169] Array keys and properties: non-index number keys, Object.keys, console.log --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + .../compiler/src/contracts/int32-elements.ts | 12 ++- .../src/frontend/lowering/lower-calls.ts | 13 ++- .../src/frontend/lowering/lower-inspect.ts | 14 +++- .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/validate.ts | 6 +- .../corpus/7060-array-numeric-keys.cjs.json | 6 ++ .../corpus/7061-array-object-keys.ts.json | 6 ++ .../7062-inspect-array-properties.ts.json | 6 ++ packages/runtime/src/scr_array.c | 46 +++++++---- packages/runtime/src/scr_array.h | 4 + packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_inspect.c | 35 +++++++- packages/runtime/src/scr_json.c | 79 +++++++++++++++++++ .../6987-array-view-named-properties.ts | 10 --- tests/corpus/7060-array-numeric-keys.cjs | 50 ++++++++++++ tests/corpus/7061-array-object-keys.ts | 28 +++++++ tests/corpus/7062-inspect-array-properties.ts | 54 +++++++++++++ .../7060-array-numeric-keys.cjs.json | 1 + .../7061-array-object-keys.ts.json | 1 + .../7062-inspect-array-properties.ts.json | 1 + tests/harness/errors.test.ts | 22 ++++++ 23 files changed, 367 insertions(+), 36 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json create mode 100644 tests/corpus/7060-array-numeric-keys.cjs create mode 100644 tests/corpus/7061-array-object-keys.ts create mode 100644 tests/corpus/7062-inspect-array-properties.ts create mode 100644 tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json create mode 100644 tests/harness/corpus-costs/7061-array-object-keys.ts.json create mode 100644 tests/harness/corpus-costs/7062-inspect-array-properties.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aa..80e0b0044 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1522,6 +1522,7 @@ export const LIB_FN_SYMS: Record = { "regex.resultProp": "scr_regex_result_prop", "array.namedKeys": "scr_arr_named_keys", "array.hasNamedKey": "scr_arr_has_named_key", + "array.hasProperties": "scr_arr_has_properties", // queueMicrotask's checked-dynamic form (borrowed dyn; a non-function // throws synchronously), the minted setImmediate value, and the // timers/promises immediate — all mark the loop live. diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index d31f14d12..d040e8c93 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -107,6 +107,7 @@ const INERT_LIB_CALLS = new Set([ "regex.resultProp", "array.namedKeys", "array.hasNamedKey", + "array.hasProperties", "process.cwd", "process.argv", "process.execArgv", diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts index 8284d40ab..ca61a8e49 100644 --- a/packages/compiler/src/contracts/int32-elements.ts +++ b/packages/compiler/src/contracts/int32-elements.ts @@ -122,7 +122,8 @@ const COPIES = new Set([ const COPY_IN = new Set(["pushSpread", "unshiftSpread", "spliceInsert"]); /** Library calls that only read array arguments. */ -const READ_ONLY_LIB = /^(console\.|inspect\.|json\.stringify|math\.|insp\.)/; +const READ_ONLY_LIB = + /^(console\.|inspect\.|json\.stringify|math\.|insp\.|array\.(hasProperties|namedKeys|hasNamedKey)$)/; /** Library calls that always throw (the value they stand for never exists). */ const THROWING_LIB = /^error\.\w*Throw$/; @@ -872,6 +873,9 @@ export class Int32ElementContracts { }; const isNumberArray = (t: IrType): boolean => t.kind === "array" && t.elem.kind === "f64"; + // Live views handed straight to a read-only library call (util.inspect + // of an array with properties) are only read through. + const readOnlyViews = new Set(); const expr = (e: IrExpr): void => { switch (e.kind) { case "assignExpr": @@ -1000,6 +1004,7 @@ export class Int32ElementContracts { case "libCall": if (!READ_ONLY_LIB.test(e.fn)) for (const a of e.args) escape(a, a.loc, `the library call ${e.fn}`); + else for (const a of e.args) if (a.kind === "dynFrom") readOnlyViews.add(a); break; case "callValue": for (const a of e.args) escape(a, a.loc, "a call through a function value"); @@ -1008,7 +1013,10 @@ export class Int32ElementContracts { case "dynFrom": // Plain arrays and records cross into dynamic code as deep copies; // class instances and live references stay connected. - if (e.liveRef === true || this.containsObject(e.value.type, new Set())) + if ( + !readOnlyViews.has(e) && + (e.liveRef === true || this.containsObject(e.value.type, new Set())) + ) escape(e.value, e.loc, "dynamic (any or unknown) code"); break; default: diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab..c6e3d0c1e 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -8354,11 +8354,15 @@ function lowerObjectStaticCall( probed?.type.kind === "union" && isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.dynConvertible(probed.type); + // An array walks its live view: the present indices, then its numeric + // and named properties. + const isArray = probed?.type.kind === "array" && lowerer.dynConvertible(probed.type); if ( isDyn || isPrimitive || isFunction || isJsUnion || + isArray || (probed && isDynTypedRefType(probed.type)) ) { const fn = @@ -8368,7 +8372,14 @@ function lowerObjectStaticCall( ? "dyn.objValues" : "dyn.objEntries"; let v = lowerer.lowerExpr(argNode); - if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) }; + if (v.type.kind !== "dyn") + v = { + kind: "dynFrom", + value: v, + ...(isArray ? { liveRef: true as const } : {}), + type: DYN, + loc: locOf(call), + }; // In TypeScript the keys come back as a native string[] and the // entries as a native [string, any][]: the checker keeps every later // write to the arrays well typed. JavaScript may store anything into diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7e..14c67f70a 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1912,13 +1912,21 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // A closed record that kept the undeclared properties of its checked // source renders through its live checked view, which holds them in the - // source's own-key order beside the current field values. + // source's own-key order beside the current field values. An array with + // properties besides its elements does too: the view lists them after + // the elements. let prependSourceGate: (() => void) | null = null; - if (t.kind === "record" && recordMayKeepSource(lowerer, t)) { + const sourceTest: IrExpr | null = + t.kind === "record" && recordMayKeepSource(lowerer, t) + ? recordSourceTest(v(), loc) + : t.kind === "array" && lowerer.dynConvertible(t) + ? { kind: "libCall", fn: "array.hasProperties", args: [v()], type: BOOL, loc } + : null; + if (sourceTest !== null) { prependSourceGate = (): void => { body.unshift({ kind: "if", - cond: recordSourceTest(v(), loc), + cond: sourceTest, then: [ ret({ kind: "libCall", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa7876579..054b16201 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3738,6 +3738,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // Whether the array (args[0]) still has the key (args[1]) from its // namedKeys list. Borrowed; never throws. "array.hasNamedKey": { argTypes: [null, STRING], result: BOOL }, + // Whether an array (args[0], any element type) has properties besides its + // elements: noncanonical numeric keys, match metadata or named + // properties. Borrowed; never throws. + "array.hasProperties": { argTypes: [null], result: BOOL }, // node:events EventEmitter: receivers are emitter-hierarchy objects and // the chaining forms (on/off/removeAll/setMax) return the receiver's // own class — program-dependent object types, checked in the libCall diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 88ccbbc33..bdfe75f2e 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4262,7 +4262,11 @@ function validateFunction( if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc); return true; } - if (e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey") { + if ( + e.fn === "array.namedKeys" || + e.fn === "array.hasNamedKey" || + e.fn === "array.hasProperties" + ) { if (e.args[0]?.type.kind !== "array") { err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json new file mode 100644 index 000000000..5fff9651a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7060-array-numeric-keys.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json new file mode 100644 index 000000000..b2830e983 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7061-array-object-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json new file mode 100644 index 000000000..891c4ffe0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7062-inspect-array-properties.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c00d1b4f6..3d7da0c37 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -4825,6 +4825,10 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { bool scr_arr_numeric_key(const char *key, size_t len, double *out) { char text[64]; if (len == 0 || len >= sizeof text) return false; + /* A number's spelling starts with a digit, '-', "NaN" or "Infinity": + * other names answer without parsing. */ + char first = key[0]; + if (!(first >= '0' && first <= '9') && first != '-' && first != 'N' && first != 'I') return false; memcpy(text, key, len); text[len] = '\0'; char *end; @@ -4838,6 +4842,11 @@ bool scr_arr_numeric_key(const char *key, size_t len, double *out) { return true; } +bool scr_arr_has_properties(const ScrArr *a) { + const ScrArrExt *x = scr_arr_ext_of(a); + return x && (x->prop_len || x->metadata); +} + void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { const ScrDyn *metadata = SCR_AX(a)->metadata; if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE && @@ -4845,21 +4854,7 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { return; metadata = SCR_AX(a)->metadata; if (!metadata) return; - for (size_t i = 0; i < metadata->v.obj.len; i++) { - const ScrDynEntry *entry = &metadata->v.obj.entries[i]; - ScrStr *key = scr_str_new(entry->key, entry->key_len); - if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) { - scr_dyn_key_set(target, key, entry->value); - } else { - /* Properties defined through a view keep their attributes. */ - ScrDyn *name = scr_dyn_new_str(key); - ScrDyn *descriptor = scr_dyn_own_descriptor(metadata, key); - if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor)); - scr_dyn_release(descriptor); - scr_dyn_release(name); - } - scr_str_release(key); - } + for (size_t i = 0; i < metadata->v.obj.len; i++) scr_dyn_copy_entry(target, metadata, &metadata->v.obj.entries[i]); } ScrArr *scr_arr_named_keys(ScrArr *a) { @@ -4917,8 +4912,27 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { } ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL; if (!named || named->v.obj.len == 0) return; + /* The view's numeric properties are copies of the native ones (every + * write to them goes to the native array), so only the others move. */ + bool numeric = false; + for (size_t i = 0; i < named->v.obj.len && !numeric; i++) { + double number; + numeric = scr_arr_numeric_key(named->v.obj.entries[i].key, named->v.obj.entries[i].key_len, &number); + } + if (numeric) { + ScrDyn *rest = scr_dyn_new_obj(); + for (size_t i = 0; i < named->v.obj.len; i++) { + const ScrDynEntry *entry = &named->v.obj.entries[i]; + double number; + if (!scr_arr_numeric_key(entry->key, entry->key_len, &number)) scr_dyn_copy_entry(rest, named, entry); + } + named = rest; + } else { + scr_dyn_retain(named); + } ScrArrExt *x = scr_arr_ext(a); ScrDyn *old = x->metadata; - x->metadata = scr_dyn_retain(named); + x->metadata = named->v.obj.len ? named : NULL; + if (!x->metadata) scr_dyn_release(named); scr_dyn_release(old); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6c41b3dbd..d08b94c14 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -333,6 +333,10 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key); * noncanonical numeric properties, then the named properties in insertion * order. +1. */ ScrArr *scr_arr_named_keys(ScrArr *a); +/* Whether the array has properties besides its elements (noncanonical + * numeric keys, match metadata, named properties): util.inspect lists + * them after the elements. */ +bool scr_arr_has_properties(const ScrArr *a); /* Installed by scr_regex.c when it records a pending match source; this * unit links without the regex runtime. Writes the pending metadata of * `a` into its dynamic copy or materializes it, answering whether it diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 02c4d828f..8974fc7ce 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -561,6 +561,9 @@ static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDy ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len); /* Own-enumerable check on a SCR_DYN_OBJ. */ bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len); +/* Stores own property `entry` of `source` on `target` with its attributes: + * plain data properties by an ordinary write, others by their descriptor. */ +void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry); /* Literal discriminator tests used before selecting a typed record layout. * Missing fields and different primitive kinds never match. Borrowed args. */ bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 223517e92..7a1685a02 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1499,14 +1499,20 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { return scr_insp_str(d->v.str); case SCR_DYN_ARR: { size_t len = d->v.arr.len; - if (len == 0) return scr_str_new("[]", 2); + /* Named and noncanonical numeric properties follow the elements as + * `key: value` entries (Node's array extras). */ + const ScrDyn *props = d->v.arr.properties; + size_t nprops = 0; + for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable; + if (len == 0 && nprops == 0) return scr_str_new("[]", 2); if (recurse > depth) return scr_str_new("[Array]", 7); scr_insp_begin(recurse + 1); /* Node's maxArrayLength (100) counts output entries; a run of holes * is one `` entry (formatSpecialArray). */ const unsigned char *presence = d->v.arr.presence; size_t i = 0; - for (size_t shown = 0; i < len && shown < 100; shown++) { + size_t shown = 0; + for (; i < len && shown < 100; shown++) { if (presence && !presence[i]) { size_t next = i + 1; while (next < len && !presence[next]) next++; @@ -1524,15 +1530,38 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(s); } bool more = i < len; + size_t entries = shown + (more ? 1 : 0); if (more) { ScrStr *m = scr_insp_more_items((double)(len - i)); scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM); scr_str_release(m); } + for (size_t k = 0; props && k < props->v.obj.len; k++) { + const ScrDynEntry *ent = &props->v.obj.entries[k]; + if (!ent->enumerable) continue; + ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + InspBuf eb = {0}; + insp_key_into(&eb, ent->key, ent->key_len); + ib_cstr(&eb, ": "); + ib_bytes(&eb, val->data, val->len); + scr_str_release(val); + ScrStr *entry = ib_take(&eb); + /* Node's grouping pads by the array element at the entry's output + * position (value[i] in groupArrayElements), not by the entry. */ + bool number = entries < len && (!presence || presence[entries]) && + d->v.arr.items[entries]->kind == SCR_DYN_NUM; + scr_insp_entry(entry, number); + scr_str_release(entry); + entries++; + } + /* Node's grouping leaves out the last entry when there are more + * entries than maxArrayLength (the `... more items` entry when the + * array has no properties). */ + bool last_out = entries > 100; ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new("[", 1); ScrStr *b1 = scr_str_new("]", 1); - ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, last_out); scr_str_release(base); scr_str_release(b0); scr_str_release(b1); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8f16630b..e8b75e5fa 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3241,6 +3241,43 @@ ScrDyn *scr_dyn_class_view_unavailable(void *ptr) { return scr_dyn_retain(scr_dyn_undefined()); } +void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry) { + ScrStr *key = scr_str_new(entry->key, entry->key_len); + if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) { + scr_dyn_key_set(target, key, entry->value); + } else { + /* Properties defined through a view keep their attributes. */ + ScrDyn *name = scr_dyn_new_str(key); + ScrDyn *descriptor = scr_dyn_own_descriptor(source, key); + if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor)); + scr_dyn_release(descriptor); + scr_dyn_release(name); + } + scr_str_release(key); +} + +static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key); + +/* A live array's view lists the native numeric properties first, then the + * named properties the snapshot copied from the array's metadata. */ +static void scr_typed_ref_view_numeric_properties(const ScrDyn *capsule, ScrDyn *view) { + const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(capsule); + ScrArr *arr = capsule->v.typed_ref.ptr; + const ScrArrExt *x = ops ? scr_arr_ext_of(arr) : NULL; + if (!x || x->prop_len == 0 || view->kind != SCR_DYN_ARR) return; + ScrDyn *named = view->v.arr.properties; + view->v.arr.properties = NULL; + for (size_t i = 0; i < x->prop_len; i++) { + double number; + if (!scr_arr_numeric_key(x->props[i].key, x->props[i].key_len, &number)) continue; + ScrDyn *value = scr_typed_ref_numeric_property(capsule, number); + if (value) scr_dyn_arr_named_put(view, x->props[i].key, x->props[i].key_len, value); + } + if (!named) return; + for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &named->v.obj.entries[i]); + scr_dyn_release(named); +} + ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { ScrDyn *capsule = (ScrDyn *)d; ScrDyn *fresh = capsule->v.typed_ref.materialize(capsule->v.typed_ref.ptr); @@ -3248,6 +3285,7 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { scr_dyn_release(fresh); return scr_dyn_retain(scr_dyn_undefined()); } + scr_typed_ref_view_numeric_properties(capsule, fresh); if (!capsule->v.typed_ref.materialized) { capsule->v.typed_ref.materialized = fresh; } else { @@ -3467,6 +3505,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { return out; } +/* A numeric property of a live array ("-1", "1.5"): +1, or NULL when the + * native array has none. */ +static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key) { + ScrArr *arr = d->v.typed_ref.ptr; + double state = scr_arr_state(arr, key); + if (state == (double)SCR_ARR_VALUE) return scr_dyn_typed_ref_array_ops(d)->get(arr, key); + if (state == (double)SCR_ARR_UNDEFINED) return scr_dyn_retain(scr_dyn_undefined()); + return NULL; +} + ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { if (d->v.typed_ref.record) return scr_typed_ref_record_get(d, key); if (scr_event_ref_hooks) { @@ -3475,6 +3523,8 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { } if (!scr_dyn_typed_ref_array_ops(d)) return NULL; if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len); + double number; + if (scr_arr_numeric_key(key->data, key->len, &number)) return scr_typed_ref_numeric_property(d, number); size_t index; if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return NULL; return scr_dyn_typed_ref_element(d, index); @@ -3484,6 +3534,9 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { if (!scr_dyn_typed_ref_array_ops(d)) return false; if (scr_typed_ref_is_length(key)) return true; + double number; + if (scr_arr_numeric_key(key->data, key->len, &number)) + return scr_arr_state(d->v.typed_ref.ptr, number) != (double)SCR_ARR_HOLE; size_t index; if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false; ScrArr *arr = d->v.typed_ref.ptr; @@ -3496,6 +3549,22 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(d); if (!ops || !ops->set) return false; + /* Numeric properties live in the native array's property table, which + * holds values of the element type. */ + double number; + if (scr_arr_numeric_key(key->data, key->len, &number)) { + if (ops->set(d->v.typed_ref.ptr, number, value)) return true; + ScrStr *kind = scr_dyn_typeof(value); + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "expected the array's element type at $["); + scr_jb_write(&message, key->data, key->len); + scr_jb_puts(&message, "], got "); + scr_jb_write(&message, kind->data, kind->len); + scr_str_release(kind); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + return true; + } size_t index; if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false; ScrArr *arr = d->v.typed_ref.ptr; @@ -6341,6 +6410,16 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { } ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (target->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(target) && key->kind == SCR_DYN_STR) { + /* A native array's numeric properties ("-1", "1.5") live in its + * property table, which holds values without attributes. */ + double number; + if (scr_arr_numeric_key(key->v.str->data, key->v.str->len, &number)) { + static const char message[] = "Object.defineProperty on a numeric property of a native array is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return NULL; + } + } if (target->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor); diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts index ee357d072..faba7dc90 100644 --- a/tests/corpus/6987-array-view-named-properties.ts +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -102,13 +102,3 @@ for (const key in visited) { console.log("visited key", key); if (key === "first") delete (visited as unknown as Bag).second; } - -// A non-configurable numeric-looking property refuses the delete and stays. -const fixed: string[] = ["a"]; -Object.defineProperty(fixed as unknown as Bag, "-3", { value: "kept", enumerable: true }); -try { - delete (fixed as unknown as Bag)["-3"]; -} catch (e) { - console.log((e as Error).name); -} -console.log((fixed as unknown as Bag)["-3"], "-3" in (fixed as unknown as Bag)); diff --git a/tests/corpus/7060-array-numeric-keys.cjs b/tests/corpus/7060-array-numeric-keys.cjs new file mode 100644 index 000000000..5a1978cd9 --- /dev/null +++ b/tests/corpus/7060-array-numeric-keys.cjs @@ -0,0 +1,50 @@ +// Number keys that are not array indices ("-1", "1.5", "NaN", "Infinity") +// are ordinary properties of an array. Writes and reads through an untyped +// function, through a string key, and through the array itself all reach +// the same property, and key walks list them after the indices. + +function put(target, key, value) { + target[key] = value; +} +function get(target, key) { + return target[key]; +} +function has(target, key) { + return key in target; +} +function drop(target, key) { + delete target[key]; +} + +const empty = []; +empty[-1] = "x"; +empty[1.5] = "y"; +empty[NaN] = "z"; +console.log(empty[-1], empty[1.5], empty[NaN], empty.length); +console.log(empty["-1"], empty["1.5"], empty["NaN"]); + +const words = ["p", "q"]; +put(words, -1, "x"); +put(words, 1.5, "y"); +put(words, NaN, "z"); +put(words, Infinity, "inf"); +console.log(get(words, -1), get(words, 1.5), get(words, NaN), get(words, Infinity)); +console.log(words[-1], words[1.5], words[NaN], words[Infinity], words["-1"], words.length); +console.log(get(words, "-1"), get(words, "1.50"), get(words, "-0"), has(words, "1.5")); +console.log(Object.keys(words), JSON.stringify(words)); +for (const key in words) console.log("key", key); + +words[-1] = "again"; +console.log(get(words, -1), words["-1"]); +put(words, "label", "named"); +console.log(get(words, "label"), get(words, -1)); +drop(words, -1); +console.log(words[-1], has(words, -1), get(words, 1.5)); +console.log(Object.keys(words)); + +const numbers = [1, 2]; +put(numbers, -1, 10); +put(numbers, 2.5, 20); +console.log(numbers[-1], get(numbers, -1), numbers[2.5], numbers.length); +numbers.push(3); +console.log(get(numbers, -1), numbers.join(",")); diff --git a/tests/corpus/7061-array-object-keys.ts b/tests/corpus/7061-array-object-keys.ts new file mode 100644 index 000000000..93691a78f --- /dev/null +++ b/tests/corpus/7061-array-object-keys.ts @@ -0,0 +1,28 @@ +// Object.keys, Object.values and Object.entries of a typed array list the +// present indices, then its numeric properties ("-1", "1.5"), then its +// named properties, as in Node. Holes are skipped. + +type Bag = Record; + +const words: string[] = ["p", "q"]; +words[-1] = "x"; +(words as unknown as Bag)["1.5"] = "y"; +(words as unknown as Bag).label = "named"; +console.log(Object.keys(words), Object.values(words), Object.entries(words)); +console.log((words as unknown as Bag)["-1"], words[1.5], "1.5" in (words as unknown as Bag)); +for (const key of Object.keys(words)) console.log(key, key.length); + +const holes: (number | undefined)[] = [1, , 3, undefined]; +console.log(Object.keys(holes), Object.values(holes), Object.entries(holes)); + +const empty: number[] = []; +console.log(Object.keys(empty).length, Object.entries(empty)); + +interface Point { + x: number; +} +const points: Point[] = [{ x: 1 }, { x: 2 }]; +console.log(Object.keys(points), Object.values(points)); +const keys: string[] = Object.keys(points); +keys.push("extra"); +console.log(keys.join("|"), Object.keys(points).length); diff --git a/tests/corpus/7062-inspect-array-properties.ts b/tests/corpus/7062-inspect-array-properties.ts new file mode 100644 index 000000000..aa18cfca9 --- /dev/null +++ b/tests/corpus/7062-inspect-array-properties.ts @@ -0,0 +1,54 @@ +// console.log of an array lists its named and numeric properties after the +// elements, as Node's util.inspect does: typed arrays written through an +// unknown view, regex match results, arrays from JSON.parse, holes, Node's +// column grouping and the 100-entry limit. + +type Bag = Record; + +const named: number[] = [1, 2, 3]; +(named as unknown as Bag).extra = "named"; +console.log(named); + +const words: string[] = ["a"]; +(words as unknown as Bag)["my-key"] = { deep: [1, { x: 1 }] }; +console.log(words, [words]); + +const empty: number[] = []; +(empty as unknown as Bag).x = 1; +console.log(empty); + +const numeric: number[] = [1, 2]; +numeric[-1] = 5; +numeric[1.5] = 6; +console.log(numeric); + +const holes: (number | undefined)[] = [1, , 3]; +(holes as unknown as Bag).tail = true; +console.log(holes); + +const grid: number[] = []; +for (let i = 0; i < 30; i++) grid.push(i * 7); +(grid as unknown as Bag).label = "grid"; +console.log(grid); + +const long: number[] = []; +for (let i = 0; i < 120; i++) long.push(i); +(long as unknown as Bag).label = "long"; +console.log(long); + +const full: number[] = []; +for (let i = 0; i < 100; i++) full.push(i); +(full as unknown as Bag).label = "full"; +console.log(full); + +const match = /b(?c)?/.exec("abc"); +console.log(match); +const plainMatch = /x/.exec("axb"); +console.log(plainMatch); + +const parsed = JSON.parse("[1,2,3,4,5,6,7,8]") as unknown as Bag; +parsed.extra = "named"; +console.log(parsed); + +const plain: number[] = [1, 2, 3]; +console.log(plain, [] as number[]); diff --git a/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json b/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json new file mode 100644 index 000000000..184d2ea9f --- /dev/null +++ b/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 6000 } diff --git a/tests/harness/corpus-costs/7061-array-object-keys.ts.json b/tests/harness/corpus-costs/7061-array-object-keys.ts.json new file mode 100644 index 000000000..b39c9d973 --- /dev/null +++ b/tests/harness/corpus-costs/7061-array-object-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5400 } diff --git a/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json b/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json new file mode 100644 index 000000000..dcd73aea7 --- /dev/null +++ b/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 6000 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca..38749c168 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1777,3 +1777,25 @@ console.log("unreachable"); ); }); }); + +describe("numeric properties of native arrays", () => { + // A native array keeps its numeric properties ("-1", "1.5") in a property + // table that holds values without attributes, so defining one with a + // descriptor refuses instead of dropping the attributes. + test("Object.defineProperty on a numeric key is a named fence", async () => { + const r = await compileAndRun( + "array-numeric-define-property", + `const words: string[] = ["a"]; +words[-1] = "x"; +console.log(words[-1]); +Object.defineProperty(words as unknown as Record, "-3", { value: "kept" }); +console.log("unreachable"); +`, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("x\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: Object\.defineProperty on a numeric property of a native array is not supported yet\n$/, + ); + }); +}); From bde0b64004a692c8254138475f7168439db3b3c4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:36:03 -0500 Subject: [PATCH 005/169] Test that move-state refuses a state file committed as a link outside the checkout A branch can commit the file a state group writes (or that the config creates from a template) as a symlink. The split tools resolve every path they read or write through confine.mjs, so move-state refuses such a file instead of appending the state through the link. The test covers that path: the group file is listed under create, exists as a link to a file outside the checkout, and the tool exits with an error and leaves the outside file unchanged. --- tests/harness/split-tools.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/harness/split-tools.test.ts b/tests/harness/split-tools.test.ts index 778ef152b..08d89e850 100644 --- a/tests/harness/split-tools.test.ts +++ b/tests/harness/split-tools.test.ts @@ -193,6 +193,30 @@ test("split-module refuses a module that links outside the checkout", () => { expect(stderr).toContain("refusing"); }); +test("move-state refuses a state file that a branch commits as a link outside the checkout", () => { + const { dir, repo } = symlinkCheckout(); + // The group file is listed under `create` and already exists as a link, so + // creation skips it and the move would append the state through the link. + symlinkSync("../../outside/linked.ts", join(repo, "src/host-state.ts")); + writeFileSync(join(dir, "state.template"), ""); + const stderr = refused( + "move-state.mjs", + { + module: "src/host.ts", + class: "Host", + dir: "src", + create: { "host-state.ts": "state.template" }, + groups: [ + { file: "host-state.ts", key: "COUNTS", interfaceName: "Counts", fields: ["count"] }, + ], + }, + dir, + repo, + ); + expect(stderr).toContain("refusing"); + expect(readFileSync(join(dir, "outside/linked.ts"), "utf8")).toBe(outsideText); +}); + test("move-state refuses to rewrite a file that links outside the checkout", () => { const { dir, repo } = symlinkCheckout(); writeFileSync(join(repo, "src/host-state.ts"), ""); From 44efd4b55bf1fb0b4500699aa5134fd829ef1c18 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:49:11 -0500 Subject: [PATCH 006/169] Reuse the cached build after whitespace and comment edits --- .../compiler/src/frontend/input-tracker.ts | 52 +-- .../src/frontend/token-equivalence.test.ts | 49 +++ .../src/frontend/token-equivalence.ts | 370 ++++++++++++++++++ packages/compiler/src/native/prepare.test.ts | 60 ++- packages/compiler/src/native/prepare.ts | 80 +++- 5 files changed, 564 insertions(+), 47 deletions(-) create mode 100644 packages/compiler/src/frontend/token-equivalence.test.ts create mode 100644 packages/compiler/src/frontend/token-equivalence.ts diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index fa2e92568..ed2f7b176 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -410,36 +410,38 @@ export function frontendInputsSemanticallyMatch( exclusions: FrontendInputExclusions = {}, ): FrontendSemanticMatch | null { if (!validFrontendInputSnapshot(snapshot)) return null; - const sourceProbes = new Map( - snapshot.probes - .filter((probe): probe is Extract => probe.op === "file") - .map((probe) => [probe.path, probe]), - ); + const sourceProbes = new Map>(); + for (const probe of snapshot.probes) if (probe.op === "file") sourceProbes.set(probe.path, probe); for (const [path, previous] of previousSources) { if (sourceProbes.get(path)?.digest !== frontendSourceDigest(previous)) return null; } const changed: FrontendSemanticChange[] = []; const currentSources = new Map(); - const adjusted: FrontendInputSnapshot = { - ...snapshot, - probes: snapshot.probes.map((probe) => { - if (probe.op !== "file") return probe; - const previous = previousSources.get(probe.path); - if (previous === undefined || frontendSourceDigest(previous) !== probe.digest) return probe; - let current: string; - try { - current = readFileSync(probe.path, "utf8"); - } catch { - return probe; - } - const currentDigest = frontendSourceDigest(current); - currentSources.set(probe.path, current); - if (currentDigest === probe.digest) return probe; - if (!isSemanticEquivalent(probe.path, previous, current)) return probe; - changed.push({ path: probe.path, previous, current }); - return { ...probe, digest: currentDigest }; - }), - }; + const probes: FrontendInputProbe[] = []; + for (const probe of snapshot.probes) { + const previous = probe.op === "file" ? previousSources.get(probe.path) : undefined; + if (probe.op !== "file" || previous === undefined) { + probes.push(probe); + continue; + } + let current: string; + try { + current = readFileSync(probe.path, "utf8"); + } catch { + return null; + } + const currentDigest = frontendSourceDigest(current); + currentSources.set(probe.path, current); + if (currentDigest === probe.digest) { + probes.push(probe); + continue; + } + // One edit that is not equivalent already rules out the match. + if (!isSemanticEquivalent(probe.path, previous, current)) return null; + changed.push({ path: probe.path, previous, current }); + probes.push({ ...probe, digest: currentDigest }); + } + const adjusted: FrontendInputSnapshot = { ...snapshot, probes }; if (!frontendInputsStillMatch(adjusted, exclusions)) return null; for (const [path, previous] of previousSources) { if (!currentSources.has(path)) { diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts new file mode 100644 index 000000000..2c7b8ea0a --- /dev/null +++ b/packages/compiler/src/frontend/token-equivalence.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from "vitest"; +import { tokenEquivalentSource } from "./token-equivalence.js"; + +const equivalent = (previous: string, current: string, path = "/src/a.ts") => + tokenEquivalentSource(path, previous, current); + +test("whitespace and plain comments are trivia", () => { + expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true); + expect(equivalent("f(a,b);\n", "f( a, /* first */ b );\n")).toBe(true); + expect(equivalent("const s = `x${a}y`;\n", "const s = `x${ a /* the value */ }y`;\n")).toBe(true); +}); + +test("tokens, literals and line breaks that change semicolon insertion are not", () => { + expect(equivalent("const a = 1;\n", "const a = 2;\n")).toBe(false); + expect(equivalent('const s = "a // b";\n', 'const s = "a // c";\n')).toBe(false); + expect(equivalent("const s = `a // ${b} /* c */`;\n", "const s = `a // ${b} /* d */`;\n")).toBe( + false, + ); + // `return` followed by a line break returns undefined. + expect(equivalent("return a;\n", "return\na;\n")).toBe(false); + expect(equivalent("return a;\n", "return /*\n*/ a;\n")).toBe(false); +}); + +test("comment-like text inside regular expressions is code", () => { + expect(equivalent("const r = /[//]/; x = 1;\n", "const r = /[//]/; x = 2;\n")).toBe(false); + expect(equivalent("if (a) /\\/*/.test(b); c = 1;\n", "if (a) /\\/*/.test(b); c = 2;\n")).toBe( + false, + ); + expect(equivalent("const q = a / b; // ratio\n", "const q = a / b; // the ratio\n")).toBe(true); + expect(equivalent("return /a/; // one\n", "return /a/; // two\n")).toBe(true); +}); + +test("comments that can affect compilation are kept", () => { + expect(equivalent("/** @type {number} */ let a;\n", "/** @type {string} */ let a;\n")).toBe( + false, + ); + expect(equivalent("// @ts-ignore\nf();\n", "f();\n")).toBe(false); + expect(equivalent('/// \n', "\n")).toBe(false); + // Every comment is kept in JavaScript, whose types come from JSDoc. + expect(equivalent("let a; // one\n", "let a; // two\n", "/src/a.js")).toBe(false); +}); + +test("text the scanner cannot classify is never equivalent", () => { + expect(equivalent("x = {} / 2; // a\n", "x = {} / 2; // b\n")).toBe(false); + expect(equivalent("++/a/.lastIndex; // a\n", "++/a/.lastIndex; // b\n")).toBe(false); + expect(equivalent("const \\u0061 = 1; // a\n", "const \\u0061 = 1; // b\n")).toBe(false); + expect(equivalent("const a = ; // a\n", "const a = ; // b\n", "/src/a.tsx")).toBe(false); + expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false); +}); diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts new file mode 100644 index 000000000..6387a0e4b --- /dev/null +++ b/packages/compiler/src/frontend/token-equivalence.ts @@ -0,0 +1,370 @@ +/* Token equivalence of two versions of a source file, for frontend cache + * reuse after edits that touch only comments and whitespace. + * + * Two versions are equivalent when they have the same tokens, in the same + * order, with line breaks between the same pairs of tokens (automatic + * semicolon insertion depends on them), and differ only in whitespace and + * in comments that cannot affect compilation. The scanner answers "not + * equivalent" whenever it cannot classify text with certainty, so a wrong + * answer can only cost a cache hit: + * + * - Every comment in a file that is not TypeScript (`.ts`, `.mts`, `.cts`) + * is kept as a token: JavaScript types come from JSDoc. TypeScript + * comments are kept when they start with `///` (references and + * directives) or contain `@` or `#` (checker directives, JSDoc tags, and + * annotations lowering reads). + * - A `/` starts a regular expression or divides according to the token + * before it. After `)` it divides unless the parenthesis closes an `if`, + * `while`, `for`, or `with` header. After `}`, `++` or `--` the scanner + * cannot tell which, so it gives up, as it does for escaped identifiers, + * which can spell keywords. + * - Template literals track their `${` substitutions on a brace stack. + * - JSX files, unterminated strings, comments, templates and regular + * expressions, and any line terminator outside ASCII give up. + * + * Plain character scans keep the scanner free of Unicode property escapes: + * any non-ASCII code unit outside strings, templates, regular expressions + * and comments belongs to the token around it, so an edit there compares + * literally. */ + +interface Token { + text: string; + comment: boolean; + lineBreakBefore: boolean; +} + +/** Keywords after which `/` starts a regular expression. */ +const EXPRESSION_KEYWORDS = new Set([ + "return", + "typeof", + "instanceof", + "in", + "of", + "new", + "delete", + "void", + "throw", + "case", + "do", + "else", + "yield", + "await", + "default", + "extends", +]); + +/** Keywords whose parenthesized header is followed by a statement. */ +const HEADER_KEYWORDS = new Set(["if", "while", "for", "with"]); + +const PUNCTUATORS = [ + ">>>=", + "...", + "===", + "!==", + "**=", + "<<=", + ">>=", + ">>>", + "&&=", + "||=", + "??=", + "=>", + "==", + "!=", + "<=", + ">=", + "&&", + "||", + "??", + "?.", + "++", + "--", + "+=", + "-=", + "*=", + "%=", + "&=", + "|=", + "^=", + "**", + "<<", + ">>", +]; + +function isTypeScript(path: string): boolean { + return path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts"); +} + +function isIdentifierCode(code: number): boolean { + return ( + (code >= 97 && code <= 122) || + (code >= 65 && code <= 90) || + (code >= 48 && code <= 57) || + code === 36 || + code === 95 || + code === 92 || + code > 127 + ); +} + +function isLineBreak(code: number): boolean { + return code === 10 || code === 13; +} + +function isSpace(code: number): boolean { + return code === 32 || code === 9 || code === 11 || code === 12 || isLineBreak(code); +} + +/** The tokens of `source`, or null when the scanner cannot classify it. */ +function scan(path: string, source: string): Token[] | null { + if (path.endsWith(".tsx") || path.endsWith(".jsx")) return null; + const typeScript = isTypeScript(path); + const tokens: Token[] = []; + // Brace kinds: true for a template substitution, false otherwise. + const braces: boolean[] = []; + // Paren kinds: true for a statement header. + const parens: boolean[] = []; + let lineBreakBefore = false; + // The previous token, and whether a `)` closed a statement header. + const last: { token: string | null } = { token: null }; + let closedHeader = false; + const push = (text: string, comment = false): void => { + tokens.push({ text, comment, lineBreakBefore }); + lineBreakBefore = false; + if (!comment) last.token = text; + }; + let index = 0; + const length = source.length; + // A shebang is one token. + if (source.startsWith("#!")) { + while (index < length && !isLineBreak(source.charCodeAt(index))) index++; + push(source.slice(0, index)); + } + /** Scans template text from `index` (after a backtick or a closing + * substitution brace) through the closing backtick or `${`. */ + const template = (start: number): boolean => { + let position = index; + while (position < length) { + const code = source.charCodeAt(position); + if (code === 92) { + position += 2; + continue; + } + if (code === 96) { + position++; + index = position; + push(source.slice(start, position)); + closedHeader = false; + return true; + } + if (code === 36 && source.charCodeAt(position + 1) === 123) { + position += 2; + index = position; + push(source.slice(start, position)); + braces.push(true); + last.token = "${"; + return true; + } + position++; + } + return false; + }; + while (index < length) { + const code = source.charCodeAt(index); + if (isSpace(code)) { + if (isLineBreak(code)) lineBreakBefore = true; + index++; + continue; + } + if (code > 127 && (code === 0x2028 || code === 0x2029 || code === 0xa0 || code === 0xfeff)) + return null; + const next = source.charCodeAt(index + 1); + // Comments. + if (code === 47 && (next === 47 || next === 42)) { + const start = index; + if (next === 47) { + while (index < length && !isLineBreak(source.charCodeAt(index))) index++; + } else { + const end = source.indexOf("*/", index + 2); + if (end < 0) return null; + index = end + 2; + } + const text = source.slice(start, index); + const semantic = + !typeScript || text.startsWith("///") || text.includes("@") || text.includes("#"); + if (semantic) push(text, true); + else if (/[\r\n]/.test(text)) lineBreakBefore = true; + continue; + } + // Strings. + if (code === 34 || code === 39) { + const start = index; + index++; + while (index < length) { + const inner = source.charCodeAt(index); + if (inner === 92) { + index += 2; + continue; + } + if (isLineBreak(inner)) return null; + index++; + if (inner === code) break; + if (index >= length) return null; + } + if (source.charCodeAt(index - 1) !== code || index - start < 2) return null; + push(source.slice(start, index)); + closedHeader = false; + continue; + } + // Templates. + if (code === 96) { + const start = index; + index++; + if (!template(start)) return null; + continue; + } + // Identifiers, keywords and numbers. + if (isIdentifierCode(code) || (code === 46 && next >= 48 && next <= 57)) { + const start = index; + index++; + while (index < length) { + const inner = source.charCodeAt(index); + if (isIdentifierCode(inner) || inner === 46) { + // A dot continues only a number (`1.5`), not member access. + if (inner === 46) { + const first = source.charCodeAt(start); + if (!((first >= 48 && first <= 57) || first === 46)) break; + } + index++; + continue; + } + // Exponent signs (`1e-5`). + if ( + (inner === 43 || inner === 45) && + (source.charCodeAt(index - 1) === 101 || source.charCodeAt(index - 1) === 69) + ) { + const first = source.charCodeAt(start); + if ((first >= 48 && first <= 57) || first === 46) { + index++; + continue; + } + } + break; + } + const word = source.slice(start, index); + // An escaped identifier can spell a keyword (`\u0072eturn`). + if (word.includes("\\")) return null; + push(word); + closedHeader = false; + continue; + } + // Division or a regular expression. + if (code === 47) { + const before = last.token; + let regex: boolean; + if (before === null) regex = true; + else if (before === "}") return null; + else if (before === ")") regex = closedHeader; + // `++/re/.lastIndex` is valid, `x++ / 2` too. + else if (before === "++" || before === "--") return null; + else if (before === "]") regex = false; + else if (/^[A-Za-z0-9_$\\]/.test(before) || before.charCodeAt(0) > 127) + regex = EXPRESSION_KEYWORDS.has(before); + else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`")) + regex = false; + else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false; + else regex = true; + if (regex) { + const start = index; + index++; + let inClass = false; + while (index < length) { + const inner = source.charCodeAt(index); + if (isLineBreak(inner)) return null; + if (inner === 92) { + index += 2; + continue; + } + if (inner === 91) inClass = true; + else if (inner === 93) inClass = false; + else if (inner === 47 && !inClass) break; + index++; + } + if (index >= length) return null; + index++; + while (index < length && isIdentifierCode(source.charCodeAt(index))) index++; + push(source.slice(start, index)); + closedHeader = false; + continue; + } + const text = next === 61 ? "/=" : "/"; + index += text.length; + push(text); + closedHeader = false; + continue; + } + // Brackets. + if (code === 40) { + parens.push(last.token !== null && HEADER_KEYWORDS.has(last.token)); + index++; + push("("); + closedHeader = false; + continue; + } + if (code === 41) { + const header = parens.pop() ?? false; + index++; + push(")"); + closedHeader = header; + continue; + } + if (code === 123) { + braces.push(false); + index++; + push("{"); + closedHeader = false; + continue; + } + if (code === 125) { + if (braces.pop() === true) { + const start = index; + index++; + if (!template(start)) return null; + continue; + } + index++; + push("}"); + closedHeader = false; + continue; + } + // Other punctuators. + let text = source[index]!; + for (const punctuator of PUNCTUATORS) + if (source.startsWith(punctuator, index)) { + text = punctuator; + break; + } + index += text.length; + push(text); + closedHeader = false; + } + if (braces.length !== 0 || parens.length !== 0) return null; + return tokens; +} + +/** Whether `current` differs from `previous` only in whitespace and in + * comments that cannot affect compilation. */ +export function tokenEquivalentSource(path: string, previous: string, current: string): boolean { + if (previous === current) return true; + const left = scan(path, previous); + if (left === null) return false; + const right = scan(path, current); + if (right === null || left.length !== right.length) return false; + for (let index = 0; index < left.length; index++) { + const a = left[index]!; + const b = right[index]!; + if (a.text !== b.text || a.comment !== b.comment) return false; + if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false; + } + return true; +} diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts index 7e22450da..35e866d6a 100644 --- a/packages/compiler/src/native/prepare.test.ts +++ b/packages/compiler/src/native/prepare.test.ts @@ -16,6 +16,22 @@ afterEach(() => { for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true }); }); +function fixtureToolchain(): NativeToolchain { + const target = nativeCodegenTarget()!; + return { + compilerVersion: "test", + target, + helper: target.helper, + helperExecutable: "unused", + helperPackageRoot: "unused", + runtimePackRoot: "unused", + linker: "unused", + linkerArgs: [], + dsymutil: "unused", + ts7Executable: ts7Executable(), + }; +} + function fixture() { const temporary = mkdtempSync(join(tmpdir(), "scriptc-native-frontend-")); scratch.push(temporary); @@ -32,19 +48,7 @@ function fixture() { }), ); writeFileSync(join(root, "package.json"), '{"type":"module"}'); - const target = nativeCodegenTarget()!; - const toolchain: NativeToolchain = { - compilerVersion: "test", - target, - helper: target.helper, - helperExecutable: "unused", - helperPackageRoot: "unused", - runtimePackRoot: "unused", - linker: "unused", - linkerArgs: [], - dsymutil: "unused", - ts7Executable: ts7Executable(), - }; + const toolchain = fixtureToolchain(); const cache = new NativeCache(join(temporary, "cache")); const options = { outPath: join(root, ".scriptc", "main"), @@ -79,6 +83,36 @@ test("warm frontend reuse survives output creation and invalidates imported sour expect(f.loads()).toBe(2); }); +test("comment and whitespace edits reuse the frontend output a fresh build reproduces", () => { + const f = fixture(); + const first = f.prepare(); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + const commented = '// The exported value.\nexport const value = /* unchanged */ "first";\n'; + writeFileSync(f.dependency, commented); + const reused = f.prepare(); + expect(f.loads()).toBe(1); + expect(reused.ok).toBe(true); + if (!reused.ok) return; + expect(reused.llvm).toBe(first.llvm); + expect(new Map(reused.sources).get(f.dependency)).toBe(commented); + // The reused output is the one the edited sources lower to. + const fresh = prepareNativeExecutable( + f.entry, + f.options, + null, + fixtureToolchain(), + (path, npmStatic, externalTypes) => runFrontend(path, loadProgram, npmStatic, externalTypes), + new NativeCache(join(f.root, "fresh-cache")), + ); + expect(fresh.ok && fresh.llvm).toBe(first.llvm); + // The refreshed entry still notices a later code edit. + writeFileSync(f.dependency, 'export const value = "second";\n'); + const edited = f.prepare(); + expect(f.loads()).toBe(2); + expect(edited.ok && edited.llvm).not.toBe(first.llvm); +}); + test("structurally invalid cached features cause a rebuild even with a valid digest", () => { const f = fixture(); expect(f.prepare().ok).toBe(true); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 6b0a2fea5..53b22cfaa 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -16,6 +16,7 @@ import type { FfiProfile } from "../ffi/ffi-manifest.js"; import type { FrontendFactory } from "../frontend/pipeline.js"; import { FrontendInputTracker, + frontendInputsSemanticallyMatch, frontendInputsStillMatch, validFrontendInputSnapshot, type FrontendInputSnapshot, @@ -23,6 +24,8 @@ import { trackedReadFile, } from "../frontend/input-tracker.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; +import { tokenEquivalentSource } from "../frontend/token-equivalence.js"; +import { llvmBytes, wtf8Bytes } from "../backend/literals.js"; import { prepareLibrary } from "../library/prepare.js"; import type { LibraryProfile } from "../library/library-profile.js"; import type { IrModule } from "../ir/ir.js"; @@ -90,18 +93,68 @@ function publishFrontendCache( if (length <= NATIVE_STRING_BYTES) cache.writeParts("frontend", key, parts); } +/** The cached input when every frontend input still matches, else the + * entry the inputs no longer match, for reuse after equivalent edits. */ function readFrontendCache( cache: NativeCache, key: string, exclusions: FrontendInputExclusions, -): NativeExecutableInput | null { +): { input: NativeExecutableInput } | { stale: FrontendCacheEntry } | null { // The entry is decoded as one string; larger entries are misses. const bytes = cache.read("frontend", key, NATIVE_STRING_BYTES); if (bytes === null) return null; const cached: unknown = JSON.parse(bytes.toString("utf8")); - if (!validCachedInput(cached) || !frontendInputsStillMatch(cached.probes, exclusions)) + if (!validCachedInput(cached)) return null; + if (!frontendInputsStillMatch(cached.probes, exclusions)) return { stale: cached }; + return { input: { ...cached.input, llvm: cached.llvm, ir: cached.ir } }; +} + +/** The stale entry's input when every changed source is token-equivalent to + * the text the entry was built from (edits to whitespace and to comments + * that cannot affect compilation; see token-equivalence.ts). Equivalent + * sources lower to the same IR apart from source locations, which only dev + * builds' debug information and the serialized IR carry. Diagnostics carry + * locations too, so an entry that reported warnings is rebuilt. */ +function equivalentFrontendInput( + stale: FrontendCacheEntry, + options: CompileRequestOptions, + exclusions: FrontendInputExclusions, +): { input: NativeExecutableInput; probes: FrontendInputSnapshot } | null { + if ( + options.optimization === "dev" || + options.sanitize === true || + options.emitIr === true || + stale.ir !== null || + (stale.input.warnings?.length ?? 0) > 0 + ) return null; - return { ...cached.input, llvm: cached.llvm, ir: cached.ir }; + const match = frontendInputsSemanticallyMatch( + stale.probes, + new Map(stale.input.sources), + tokenEquivalentSource, + exclusions, + ); + if (match === null || match.changed.length === 0) return null; + // Runtime fences and unsupported-construct messages name their source as + // `[ at :]`, and an equivalent edit can move the line. + if ( + match.changed.some(({ path }) => + stale.llvm.includes(` at ${llvmBytes(wtf8Bytes(path), false)}:`), + ) + ) + return null; + return { + input: { + ...stale.input, + llvm: stale.llvm, + ir: null, + sources: stale.input.sources.map(([path, text]) => [ + path, + match.currentSources.get(path) ?? text, + ]), + }, + probes: match.snapshot, + }; } function implementationIdentity(toolchain: NativeToolchain): string { @@ -260,10 +313,19 @@ export function prepareNativeExecutable( .sort(([a], [b]) => a.localeCompare(b)), }), ); - const input = readFrontendCache(cache, key, exclusions); - if (input !== null) { + const cached = readFrontendCache(cache, key, exclusions); + if (cached !== null && "input" in cached) { timing("frontend-cache-hit"); - return input; + return cached.input; + } + const equivalent = + cached === null || outputKind === "ir" + ? null + : equivalentFrontendInput(cached.stale, options, exclusions); + if (equivalent !== null) { + publishFrontendCache(cache, key, equivalent.probes, equivalent.input); + timing("frontend-cache-equivalent"); + return equivalent.input; } } catch { key = null; @@ -373,10 +435,10 @@ export function prepareNativeLibrary( .sort(([a], [b]) => a.localeCompare(b)), }), ); - const input = readFrontendCache(cache, key, exclusions); - if (input !== null) { + const cached = readFrontendCache(cache, key, exclusions); + if (cached !== null && "input" in cached) { timing("frontend-cache-hit"); - return input; + return cached.input; } } catch { key = null; From cda76716a0557ef05232967ce7721ea8a1dd1d25 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:51:17 -0500 Subject: [PATCH 007/169] Enumerate each record in the key order its object literal wrote Record shapes are interned by field set, and their key order was one table per shape. When object literals of one shape wrote the keys in different orders (`{ id, name }` and `{ name, id }`), every value enumerated in the first type's order, and anonymous literal types got no warning. - The key-order witness pass gives such a shape its orders (IrRecordShape.orderVariants, the declared table first) and a hidden number field, %order, that each value sets to the index of its order: a literal of explicit keys stores its own order, a copy from a shape with one order stores that order, and spreads, rest bindings and copies of values with orders read their source's index. Allocation zeroes the field, so checked conversions and runtime producers keep the table as before, and clones copy it. - A write of an optional key a value lacks moves the key to the end of that value's order, as in Node: the orders include each order such writes reach, and the write site updates the index first, evaluating a receiver other than a binding once. - Object.keys, Object.values, Object.entries, for-in, util.inspect, JSON.stringify, the record-to-dynamic conversion (structuredClone, postMessage, untyped views) and live record views branch on the index. The dynamic conversion and checked conversions leave the field out. - A shape keeps its table, and SC6002 still reports its literals, when a construction's order cannot be traced: a spread of a value of unknown order, a clone that adds an optional key, a copy whose source is not a binding, or more than 16 orders. Corpus 7122 to 7128 check each surface against Node. --- docs/content/docs/coverage.mdx | 2 +- .../record-identity-and-property-order.mdx | 7 +- packages/compiler/src/backend/llvm/dyn.ts | 149 +++--- .../compiler/src/backend/llvm/expr-context.ts | 3 + .../src/backend/llvm/expr-stream-bridges.ts | 131 +++-- .../compiler/src/backend/llvm/key-orders.ts | 49 ++ packages/compiler/src/backend/llvm/walkers.ts | 139 +++--- .../src/frontend/lowering/divergences.ts | 5 + .../lowering/key-order-witness.test.ts | 118 ++++- .../frontend/lowering/key-order-witness.ts | 471 +++++++++++++++++- .../src/frontend/lowering/lower-calls.ts | 24 +- .../src/frontend/lowering/lower-inspect.ts | 12 +- packages/compiler/src/frontend/type-mapper.ts | 17 +- .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 18 + .../tests/corpus/7122-key-order-json.ts.json | 6 + ...7123-key-order-keys-values-entries.ts.json | 6 + .../corpus/7124-key-order-for-in.ts.json | 6 + .../corpus/7125-key-order-inspect.ts.json | 6 + .../corpus/7126-key-order-copies.ts.json | 6 + .../corpus/7127-key-order-dynamic.ts.json | 6 + .../corpus/7128-key-order-late-writes.ts.json | 6 + tests/corpus/7122-key-order-json.ts | 31 ++ .../7123-key-order-keys-values-entries.ts | 19 + tests/corpus/7124-key-order-for-in.ts | 15 + tests/corpus/7125-key-order-inspect.ts | 12 + tests/corpus/7126-key-order-copies.ts | 19 + tests/corpus/7127-key-order-dynamic.ts | 30 ++ tests/corpus/7128-key-order-late-writes.ts | 27 + .../corpus-costs/7122-key-order-json.ts.json | 1 + ...7123-key-order-keys-values-entries.ts.json | 1 + .../7124-key-order-for-in.ts.json | 1 + .../7125-key-order-inspect.ts.json | 1 + .../7126-key-order-copies.ts.json | 1 + .../7127-key-order-dynamic.ts.json | 1 + .../7128-key-order-late-writes.ts.json | 1 + 36 files changed, 1147 insertions(+), 201 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/key-orders.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json create mode 100644 tests/corpus/7122-key-order-json.ts create mode 100644 tests/corpus/7123-key-order-keys-values-entries.ts create mode 100644 tests/corpus/7124-key-order-for-in.ts create mode 100644 tests/corpus/7125-key-order-inspect.ts create mode 100644 tests/corpus/7126-key-order-copies.ts create mode 100644 tests/corpus/7127-key-order-dynamic.ts create mode 100644 tests/corpus/7128-key-order-late-writes.ts create mode 100644 tests/harness/corpus-costs/7122-key-order-json.ts.json create mode 100644 tests/harness/corpus-costs/7123-key-order-keys-values-entries.ts.json create mode 100644 tests/harness/corpus-costs/7124-key-order-for-in.ts.json create mode 100644 tests/harness/corpus-costs/7125-key-order-inspect.ts.json create mode 100644 tests/harness/corpus-costs/7126-key-order-copies.ts.json create mode 100644 tests/harness/corpus-costs/7127-key-order-dynamic.ts.json create mode 100644 tests/harness/corpus-costs/7128-key-order-late-writes.ts.json diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index ae36f736d..258e55ac8 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -95,7 +95,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them. - `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity. -- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. +- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 0381803f6..cc16f74d5 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -10,9 +10,10 @@ A type whose values are only read field by field, from fields the class does not Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference. -`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. -The order matches Node.js when every value of the type is created in the same order. -When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js. +scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. +When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last. +A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders. A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6911ef98b..c48468886 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,3 +1,4 @@ +import { emitPerKeyOrder } from "./key-orders.js"; import { typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit, sizeToDouble } from "./common.js"; @@ -37,6 +38,8 @@ import { accessorSlotProp, DYN_HANDLE_KINDS, isDirentShape, + KEY_ORDER_FIELD, + shapeKeyOrders, isDynTypedRefType, isRefCounted, isUnitType, @@ -1044,9 +1047,10 @@ export class LlDyn { } B.condBr(readable, l0, fail); B.startBlock(l0); - // dyn ('unknown') fields match ANY value, present or missing. + // dyn ('unknown') fields match ANY value, present or missing. The + // order index is no key. for (const f of shape.fields) { - if (f.type.kind === "dyn") continue; + if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); const lNext = B.newLabel("dm.n"); this.pendingBail( @@ -1228,9 +1232,10 @@ export class LlDyn { return ( shape.fields.every( (f) => - !f.name.startsWith("%") && - f.name !== "__proto__" && - this.jsonSchemaSupported(f.type, visiting, true), + f.name === KEY_ORDER_FIELD || + (!f.name.startsWith("%") && + f.name !== "__proto__" && + this.jsonSchemaSupported(f.type, visiting, true)), ) && (!index || this.jsonSchemaSupported(index, visiting)) ); @@ -1315,7 +1320,9 @@ export class LlDyn { const base = host.nullableUnions.fieldBase(t.shapeId); const index = new Map(shape.fields.map((f, i) => [f.name, i + base])); // The runtime names kept-source keys by this index (record-sources.ts). - const order = recordSourceFieldOrder(shape); + // The order index is no key: parsed values keep the table's order + // and their source's own order. + const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD); if (recordMayKeepSource(shape)) this.keepsRecordSources = true; const fieldTy = `{ ptr, ${S}, ${S}, ptr }`; const entries = order.map((fieldName) => { @@ -2058,7 +2065,10 @@ export class LlDyn { } B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`); if (recursive) this.recordBuilt(B, name, "%r0"); + // The order index stays 0: the source's own order is kept below + // when it differs from the table. for (const f of shape.fields) { + if (f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); this.pendingBail( B, @@ -2789,70 +2799,79 @@ export class LlDyn { host.declare(`declare ptr @scr_dyn_new_obj()`); B.line(`${d} = call ptr @scr_dyn_new_obj()`); } - // Keys insert in DECLARED order (JS insertion order); internal - // '%'-fields follow so a record→dyn→record round trip keeps them. + // Keys insert in DECLARED order (JS insertion order), or in the + // value's own order for a shape with orderVariants; internal + // '%'-fields follow so a record→dyn→record round trip keeps them + // (all but the order index, which is no key). const byName = new Map(shape.fields.map((f) => [f.name, f])); // An accessor property is one key whose getter and setter are the // slot closures (see accessorSlotProp), in accessorOrder's place. - const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - const accessors = new Set( - shape.accessorOrder === undefined - ? [] - : shape.fields.flatMap((f) => { - const slot = accessorSlotProp(f.name); - return slot === null ? [] : [slot.prop]; - }), - ); - type DynField = { name: string; type: IrType } | { accessor: string }; - const dynFields: DynField[] = [ - ...order.flatMap((n) => { - if (accessors.has(n)) return [{ accessor: n } as DynField]; - const f = byName.get(n); - return f === undefined ? [] : [f as DynField]; - }), - ...shape.fields.filter( - (f) => - !inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null), - ), - ]; - for (const entry of dynFields) { - if ("accessor" in entry) { - host.declare( - `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, - ); - const boxed = (kind: "get" | "set"): string => { - const slot = byName.get(`%${kind}:${entry.accessor}`); - if (slot === undefined) return "null"; - const fv = loadFieldOf(slot.name, slot.type); - const conv = B.tmp(); - B.line( - `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, - ); - return conv; - }; - const getter = boxed("get"); - const setter = boxed("set"); - B.line( - `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, - ); - continue; - } - const f = entry; - const klen = Buffer.byteLength(f.name, "utf8"); - const fv = loadFieldOf(f.name, f.type); - // An ABSENT optional field contributes no key. - const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; - const conv = B.tmp(); - B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); - B.line( - `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + const orders = + shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape); + const perOrder = (order: readonly string[]): void => { + const inOrder = new Set(order); + const accessors = new Set( + shape.accessorOrder === undefined + ? [] + : shape.fields.flatMap((f) => { + const slot = accessorSlotProp(f.name); + return slot === null ? [] : [slot.prop]; + }), ); - if (lSkip !== null) { - B.br(lSkip); - B.startBlock(lSkip); + type DynField = { name: string; type: IrType } | { accessor: string }; + const dynFields: DynField[] = [ + ...order.flatMap((n) => { + if (accessors.has(n)) return [{ accessor: n } as DynField]; + const f = byName.get(n); + return f === undefined ? [] : [f as DynField]; + }), + ...shape.fields.filter( + (f) => + !inOrder.has(f.name) && + f.name !== KEY_ORDER_FIELD && + !(accessors.size > 0 && accessorSlotProp(f.name) !== null), + ), + ]; + for (const entry of dynFields) { + if ("accessor" in entry) { + host.declare( + `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, + ); + const boxed = (kind: "get" | "set"): string => { + const slot = byName.get(`%${kind}:${entry.accessor}`); + if (slot === undefined) return "null"; + const fv = loadFieldOf(slot.name, slot.type); + const conv = B.tmp(); + B.line( + `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + ); + return conv; + }; + const getter = boxed("get"); + const setter = boxed("set"); + B.line( + `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, + ); + continue; + } + const f = entry; + const klen = Buffer.byteLength(f.name, "utf8"); + const fv = loadFieldOf(f.name, f.type); + // An ABSENT optional field contributes no key. + const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; + const conv = B.tmp(); + B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); + B.line( + `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + ); + if (lSkip !== null) { + B.br(lSkip); + B.startBlock(lSkip); + } } - } + }; + if (orders.length === 1) perOrder(orders[0]!); + else emitPerKeyOrder(B, shape, base, "%v", perOrder); // A Dirent row takes Node's Dirent prototype (the type probes) and // its Symbol(type) key; %dtype stays readable but non-enumerable. if (isDirentShape(shape)) { diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index 9dd6bbcac..af1a9a486 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -32,6 +32,9 @@ export interface LlStreamTypedRefAdapter { arrayOps?: string; /** `@sym` of the ScrDynTypedRecordOps table for live record capsules. */ recordOps?: string; + /** For a shape with orderVariants: the defined function that binds a + * live record capsule the table of its value's key order. */ + recordBind?: string; /** `@sym` of the ScrDynTypedClassOps table for class instance capsules. */ classOps?: string; } diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a0a0cf24a..b64204122 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -13,6 +13,8 @@ import { classFieldAttributesName, isClassOwnEnumerableFieldName, isDirentShape, + KEY_ORDER_FIELD, + shapeKeyOrders, isDynTypedRefType, isRefCounted, shapeHasAccessorSlots, @@ -44,6 +46,7 @@ import type { NullableRefField } from "./nullable-fields.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; import { recordMayKeepSource } from "./record-sources.js"; +import { emitPerKeyOrder } from "./key-orders.js"; /** An owned (+1) union value for a nullable-pointer class field slot `p` * (the unit arm's immortal instance, or a fresh box around a retained @@ -1054,52 +1057,68 @@ export function streamTypedRefMaterializeAdapter( const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_obj()`); const byName = new Map(shape.fields.map((field) => [field.name, field])); - const order = shape.declaredOrder ?? shape.fields.map((field) => field.name); - const inOrder = new Set(order); - const fields = [ - ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), - ...shape.fields.filter((field) => !inOrder.has(field.name)), - ]; + // A shape with orderVariants has one field list per key order; its + // order index is no key. + const orders = shapeKeyOrders(shape); + const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => { + const inOrder = new Set(order); + return [ + ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), + ...shape.fields.filter( + (field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD, + ), + ]; + }; // Published before the field converters, like the array hooks below. // A Dirent row reads through its finished view (the prototype and // the non-enumerable %dtype). const dirent = isDirentShape(shape); - if (!dirent && liveRecordOpsEligible(fields)) { - adapter.recordOps = `@${snapshot}_rops`; - liveRecordOps(host, t, snapshot, fields); - } - for (const field of fields) { - const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); - const fieldPtr = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, - ); - const fieldValue = loadStructField( - B, - host.sizeType, - fieldPtr, - field.type, - host.inlineUnions.record(field.type), - ); - // An ABSENT optional field contributes no key. - const undefinedTag = undefinedArmTag(field.type, host.unionsById); - let skip: string | null = null; - if (undefinedTag >= 0 && field.type.kind === "union") { - const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); - skip = B.newLabel("live.record.absent"); - const set = B.newLabel("live.record.set"); - B.condBr(absent, skip, set); - B.startBlock(set); - } - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); - B.line( - `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); + if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) { + if (orders.length === 1) { + adapter.recordOps = `@${snapshot}_rops`; + liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!)); + } else { + orders.forEach((order, i) => + liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)), + ); + adapter.recordBind = `${snapshot}_bind`; + liveRecordOrderBind(host, shape, snapshot); } } + emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => { + for (const field of fieldsOf(order)) { + const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); + const fieldPtr = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, + ); + const fieldValue = loadStructField( + B, + host.sizeType, + fieldPtr, + field.type, + host.inlineUnions.record(field.type), + ); + // An ABSENT optional field contributes no key. + const undefinedTag = undefinedArmTag(field.type, host.unionsById); + let skip: string | null = null; + if (undefinedTag >= 0 && field.type.kind === "union") { + const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); + skip = B.newLabel("live.record.absent"); + const set = B.newLabel("live.record.set"); + B.condBr(absent, skip, set); + B.startBlock(set); + } + const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + B.line( + `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); + } + } + }); if (dirent) { host.declare(`declare void @scr_dyn_dirent_finish(ptr)`); B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`); @@ -1366,6 +1385,32 @@ function liveRecordOps( ); } +/** `@_bind(ptr capsule, ptr unused)` for a shape with + * orderVariants: binds the capsule the record ops of its value's key order + * (the `_o_rops` tables liveRecordOps made). */ +function liveRecordOrderBind( + host: LlvmEmitterContext, + shape: IrRecordShape, + snapshot: string, +): void { + host.declare(`declare void @scr_dyn_typed_ref_bind_record(ptr, ptr)`); + const B = new BlockBuilder(); + // The capsule's native value (ScrDyn v.typed_ref.ptr, byte 16 on every + // target). + B.line(`%pp = getelementptr inbounds i8, ptr %c, i64 16`); + B.line(`%p = load ptr, ptr %pp`); + emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => { + B.line(`call void @scr_dyn_typed_ref_bind_record(ptr %c, ptr @${snapshot}_o${i}_rops)`); + }); + B.terminate(`ret void`); + host.resolveThunkDefs.push( + `define internal void @${snapshot}_bind(ptr %c, ptr %unused) ${FN_ATTRS} { ; live record ops by key order`, + B.render(), + `}`, + ``, + ); +} + /** Compiled classes whose instance views have no prototype object. Runtime * classes and JavaScript classes with prototype data answer through their * own views. */ @@ -1437,7 +1482,9 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM * records (1 array, 2 record, 3 class). */ export function liveRefView( adapter: LlStreamTypedRefAdapter, -): { bind: string; ops: string; code: 1 | 2 | 3 } | null { +): { bind: string; ops: string; code: 1 | 2 | 3; defined?: true } | null { + if (adapter.recordBind !== undefined) + return { bind: adapter.recordBind, ops: "null", code: 2, defined: true }; if (adapter.classOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_class", ops: adapter.classOps, code: 3 }; if (adapter.recordOps !== undefined) @@ -1455,7 +1502,7 @@ export function bindLiveArrayOps( ): string[] { const view = liveRefView(adapter); if (view === null) return []; - host.declare(`declare void @${view.bind}(ptr, ptr)`); + if (view.defined !== true) host.declare(`declare void @${view.bind}(ptr, ptr)`); return [`call void @${view.bind}(ptr ${capsule}, ptr ${view.ops})`]; } diff --git a/packages/compiler/src/backend/llvm/key-orders.ts b/packages/compiler/src/backend/llvm/key-orders.ts new file mode 100644 index 000000000..9dbda1414 --- /dev/null +++ b/packages/compiler/src/backend/llvm/key-orders.ts @@ -0,0 +1,49 @@ +/* Key-order dispatch for shapes whose values record the order their keys + * were written in (IrRecordShape.orderVariants): an emitter that walks a + * record's keys emits one walk per order and branches on the value's + * KEY_ORDER_FIELD. */ +import type { IrRecordShape } from "../../ir/ir.js"; +import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js"; +import { mangleRecordStruct } from "../mangle.js"; +import type { BlockBuilder } from "./blocks.js"; + +/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a + * switch on the value's order index when the shape has several. Each walk + * starts in its own block, and the code after this call continues in the + * block they all branch to. */ +export function emitPerKeyOrder( + B: BlockBuilder, + shape: IrRecordShape, + fieldBase: number, + rec: string, + walk: (order: readonly string[], index: number) => void, +): void { + const orders = shapeKeyOrders(shape); + if (orders.length === 1) { + walk(orders[0]!, 0); + return; + } + const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase; + const p = B.tmp(); + const n = B.tmp(); + const i = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`, + ); + B.line(`${n} = load double, ptr ${p}`); + B.line(`${i} = fptosi double ${n} to i32`); + const labels = orders.map(() => B.newLabel("ko")); + const join = B.newLabel("ko.join"); + B.terminate( + `switch i32 ${i}, label %${labels[0]} [ ${labels + .slice(1) + .map((label, k) => `i32 ${k + 1}, label %${label}`) + .join(" ")} ]`, + ); + orders.forEach((order, k) => { + B.startBlock(labels[k]!); + walk(order, k); + B.br(join); + }); + B.startBlock(join); +} diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 46dbc86e1..25e63251e 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -1,3 +1,4 @@ +import { emitPerKeyOrder } from "./key-orders.js"; import { f64Lit } from "./common.js"; import { InternalCompilerError } from "../../errors.js"; /* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror @@ -311,78 +312,80 @@ export class LlWalkers { if (cyclic) this.jbLeave(B); return; } - // Fields serialize in DECLARED order (JS insertion order); internal + // Fields serialize in DECLARED order (JS insertion order), or in the + // value's own order for a shape with orderVariants; internal // '%'-fields stay hidden — jsonWriteHelper's contract, ported comment // and all. - const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { - throw new InternalCompilerError( - `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, - ); - } - const byName = new Map(shape.fields.map((f) => [f.name, f])); - // Function-valued properties are omitted, as JSON.stringify omits them. - const emitFields = order - .map((n) => byName.get(n)) - .filter((f) => f !== undefined) - .filter((f) => f.type.kind !== "func"); - const droppable = - emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || - !!shape.indexValue; - this.putc(B, "%b", "123"); // '{' - if (!droppable) { - emitFields.forEach((f, i) => { - this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => { + const inOrder = new Set(order); + if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { + throw new InternalCompilerError( + `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, ); - }); - } else { - const first = B.slot(); - B.entryAllocas.push(`${first} = alloca i1`); - B.line(`store i1 true, ptr ${first}`); - const comma = (): void => { - const isf = B.tmp(); - const lc = B.newLabel("jwc.c"); - const lj = B.newLabel("jwc.j"); - B.line(`${isf} = load i1, ptr ${first}`); - B.condBr(isf, lj, lc); - B.startBlock(lc); - this.putc(B, "%b", "44"); // ',' - B.br(lj); - B.startBlock(lj); - B.line(`store i1 false, ptr ${first}`); - }; - for (const f of emitFields) { - const utag = undefinedArmTag(f.type, this.host.unionsById); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - let skip: string | null = null; - if (utag >= 0 && f.type.kind === "union") { - // Undefined-valued field: dropped, like Node. - const tag = this.unionTag(B, v, f.type.unionId); - const isu = B.tmp(); - B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); - const lw = B.newLabel("jwf.w"); - skip = B.newLabel("jwf.s"); - B.condBr(isu, skip, lw); - B.startBlock(lw); - } - comma(); - this.puts(B, "%b", jsonObjectKeyLabel(f.name)); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); - } } - if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); - } + const byName = new Map(shape.fields.map((f) => [f.name, f])); + // Function-valued properties are omitted, as JSON.stringify omits them. + const emitFields = order + .map((n) => byName.get(n)) + .filter((f) => f !== undefined) + .filter((f) => f.type.kind !== "func"); + const droppable = + emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || + !!shape.indexValue; + this.putc(B, "%b", "123"); // '{' + if (!droppable) { + emitFields.forEach((f, i) => { + this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + }); + } else { + const first = B.slot(); + B.entryAllocas.push(`${first} = alloca i1`); + B.line(`store i1 true, ptr ${first}`); + const comma = (): void => { + const isf = B.tmp(); + const lc = B.newLabel("jwc.c"); + const lj = B.newLabel("jwc.j"); + B.line(`${isf} = load i1, ptr ${first}`); + B.condBr(isf, lj, lc); + B.startBlock(lc); + this.putc(B, "%b", "44"); // ',' + B.br(lj); + B.startBlock(lj); + B.line(`store i1 false, ptr ${first}`); + }; + for (const f of emitFields) { + const utag = undefinedArmTag(f.type, this.host.unionsById); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + let skip: string | null = null; + if (utag >= 0 && f.type.kind === "union") { + // Undefined-valued field: dropped, like Node. + const tag = this.unionTag(B, v, f.type.unionId); + const isu = B.tmp(); + B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); + const lw = B.newLabel("jwf.w"); + skip = B.newLabel("jwf.s"); + B.condBr(isu, skip, lw); + B.startBlock(lw); + } + comma(); + this.puts(B, "%b", jsonObjectKeyLabel(f.name)); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); + } + } + if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); + } + }); this.putc(B, "%b", "125"); // '}' if (cyclic) this.jbLeave(B); } diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 1c362cb09..cf3c632d7 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -724,6 +724,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { + // Values of a shape with orderVariants enumerate in the order their + // literal wrote (key-order-witness.ts). + const mapped = safely(() => lowerer.mapTypeOf(contextual)); + if (mapped?.kind === "record" && lowerer.shapes.get(mapped.shapeId)?.orderVariants !== undefined) + return null; // A shape whose table the witness pass re-chose (key-order-witness.ts) // enumerates that table, not the checker's declaration order. const table = witnessTable(lowerer, contextual); 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 de16e5d61..56b65a3d3 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -1,5 +1,12 @@ import { expect, test } from "vitest"; -import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; +import { + F64, + KEY_ORDER_FIELD, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js"; import { ShapeRegistry } from "../type-mapper.js"; import { @@ -21,7 +28,7 @@ interface Fixture { notes: KeyOrderNotes; record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; - run(body: IrStmt[]): void; + run(body: IrStmt[]): IrFunction; order(type: IrType & { kind: "record" }): string[] | undefined; } @@ -35,6 +42,14 @@ function fixture(): Fixture { classes: new Map(), armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, + recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ + kind: "recordHas", + obj, + shapeId, + field, + type: { kind: "bool" }, + loc, + }), } as unknown as Lowerer; const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ kind: "record", @@ -55,15 +70,20 @@ function fixture(): Fixture { segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), ); }; - const run = (body: IrStmt[]): void => { + const run = (body: IrStmt[]): IrFunction => { const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; applyKeyOrderWitnesses(lowerer, [fn]); + return fn; }; const order = (type: IrType & { kind: "record" }): string[] | undefined => shapes.get(type.shapeId)?.declaredOrder; return { lowerer, shapes, notes, record, literal, run, order }; } +/** The order index a literal stores (KEY_ORDER_FIELD), when it stores one. */ +const orderIndexOf = (lit: IrExpr): IrExpr | undefined => + lit.kind === "recordLit" ? lit.fields.find((x) => x.name === KEY_ORDER_FIELD)?.value : undefined; + const observe = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, @@ -109,7 +129,7 @@ test("literals that agree with the table leave the same array in place", () => { expect(f.notes.changed.size).toBe(0); }); -test("values built in two orders keep today's table", () => { +test("values built in two orders each record their literal's order", () => { const f = fixture(); const ab = f.record( [ @@ -118,8 +138,71 @@ test("values built in two orders keep today's table", () => { ], ["a", "b"], ); - f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); + const ba = f.literal(ab, ["b", "a"]); + const aOrder = f.literal(ab, ["a", "b"]); + f.run([observe(ba), observe(aOrder)]); expect(f.order(ab)).toEqual(["a", "b"]); + const shape = f.shapes.get(ab.shapeId)!; + expect(shape.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); + expect(shape.fields.map((x) => x.name)).toEqual([KEY_ORDER_FIELD, "a", "b"]); + expect(orderIndexOf(ba)).toMatchObject({ kind: "numLit", value: 1 }); + expect(orderIndexOf(aOrder)).toMatchObject({ kind: "numLit", value: 0 }); +}); + +test("a copy of a value with orders stores its source's index", () => { + const f = fixture(); + const abc = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["a", "b", "c"], + ); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const read = (field: string): IrExpr => ({ + kind: "recordGet", + obj: ref("x", abc), + shapeId: abc.shapeId, + field, + type: F64, + loc, + }); + const rest = f.notes.copy( + { + kind: "recordLit", + fields: [ + { name: "a", value: read("a") }, + { name: "b", value: read("b") }, + ], + type: ab, + loc, + }, + abc.shapeId, + ); + f.run([ + decl("x", f.literal(abc, ["c", "b", "a"])), + observe(f.literal(abc, ["a", "b", "c"])), + observe(rest), + ]); + expect(f.shapes.get(ab.shapeId)!.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); + expect(orderIndexOf(rest)).toMatchObject({ + kind: "recordGet", + obj: { kind: "varRef", localId: "x" }, + field: KEY_ORDER_FIELD, + }); }); test("an unobserved shape keeps today's table", () => { @@ -307,7 +390,7 @@ test("straight-line writes of late-only keys keep program order", () => { expect(f.order(o)).toEqual(["id", "y", "x"]); }); -test("late-only keys written through different bindings keep today's table", () => { +test("late-only keys written through different bindings move to the end per value", () => { const f = fixture(); const o = f.record( [ @@ -324,9 +407,13 @@ test("late-only keys written through different bindings keep today's table", () observe(ref("o", o)), ]); expect(f.order(o)).toEqual(["id", "x", "y"]); + expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ + ["id", "x", "y"], + ["id", "y", "x"], + ]); }); -test("a late-only key written in a loop over an outer binding keeps today's table", () => { +test("a late-only key written in a loop guards the write with its move", () => { const f = fixture(); const o = f.record( [ @@ -336,7 +423,7 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl ], ["id", "x", "y"], ); - f.run([ + const body: IrStmt[] = [ decl("o", f.literal(o, ["id"])), set(ref("o", o), "y"), { @@ -346,11 +433,18 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl loc, }, observe(ref("o", o)), - ]); + ]; + const fn = f.run(body); expect(f.order(o)).toEqual(["id", "x", "y"]); + const loop = fn.body.find((st) => st.kind === "while") as IrStmt & { kind: "while" }; + const guarded = loop.body[0]!; + expect(guarded.kind).toBe("block"); + const [move, write] = (guarded as IrStmt & { kind: "block" }).body; + expect(move).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); + expect(write).toMatchObject({ kind: "recordSet", field: "x" }); }); -test("re-adding a key a literal creates earlier keeps today's table", () => { +test("re-adding a key a literal creates earlier moves it to the end of that value", () => { const f = fixture(); const o = f.record( [ @@ -369,4 +463,8 @@ test("re-adding a key a literal creates earlier keeps today's table", () => { }; f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]); expect(f.order(o)).toEqual(["a", "b"]); + expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); }); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 2fc9a6cbd..c35860d43 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -41,11 +41,56 @@ * value: that shape, and every shape copied from it, keeps today's table * (and today's SC6002 warnings). Shapes that no order surface observes * keep today's table too. */ -import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js"; -import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; -import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; +import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; +import { + BOOL, + F64, + KEY_ORDER_FIELD, + shapeHasAccessorSlots, + typeEquals, + UNDEFINED_T, +} from "../../ir/ir.js"; +import { + everyExprChild, + everyStmtChild, + everyTypeChild, + transformStmtList, +} from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.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 + * branch per order (walks[i] for order i) on the value's KEY_ORDER_FIELD. + * `walks` follows shapeKeyOrders. */ +export function perKeyOrder( + shape: IrRecordShape, + ref: IrExpr, + walks: readonly IrStmt[][], + loc: SrcLoc, +): IrStmt[] { + let tail = walks[0]!; + for (let i = walks.length - 1; i >= 1; i--) { + const index: IrExpr = { + kind: "recordGet", + obj: ref, + shapeId: shape.id, + field: KEY_ORDER_FIELD, + type: F64, + loc, + }; + const cond: IrExpr = { + kind: "bin", + op: "===", + left: index, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }; + tail = [{ kind: "if", cond, then: walks[i]!, else_: tail, loc }]; + } + return tail; +} + /** One property of an object literal, in source order. */ export type KeyOrderSegment = /** An explicit key (`definite`), or a conditional spread's key, which @@ -708,4 +753,424 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; notes.changed.add(id); } + + // ── 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 + // in (KEY_ORDER_FIELD), when every construction's order is known + // statically or copied from a value that records its own: + // - a literal of explicit keys writes its order; + // - a copy from a shape with one resolved order writes that order; + // - a copy from a value of a shape with orders writes the source's + // order, filtered to the copy's keys (rest bindings, width copies); + // - clones copy the field, and every other construction (synthesized + // literals, checked conversions, runtime producers) leaves 0, today's + // table, as before. + // A write of an optional key a value lacks moves the key to the end of + // the value's order, as in Node: the orders include every order such + // writes reach, and each write site updates the index first. + const MAX_ORDERS = 16; + const unresolved = (id: string): boolean => result.get(id) === "unknown"; + /** `order` over every key of `table`: keys the order lacks follow it. */ + const complete = (order: readonly string[], table: readonly string[]): string[] => { + const keys = new Set(table); + const out = order.filter((k) => keys.has(k)); + const seen = new Set(out); + return [...out, ...table.filter((k) => !seen.has(k))]; + }; + const literalOrder = (segments: readonly KeyOrderSegment[]): string[] | null => { + const out: string[] = []; + for (const segment of segments) { + if (segment.kind !== "key" || !segment.definite) return null; + if (!isArrayIndex(segment.name)) out.push(segment.name); + } + return out; + }; + /** `{ ...value }` of a value of the literal's own shape: a copy of the + * value's order. */ + const selfSpread = (segments: readonly KeyOrderSegment[], id: string): boolean => + segments.length === 1 && + segments[0]!.kind === "spread" && + segments[0]!.shapeId === id && + !segments[0]!.maybeAbsent; + const sameOrder = (a: readonly string[], b: readonly string[]): boolean => + a.join("\0") === b.join("\0"); + const indexOfOrder = (orders: readonly string[][], order: readonly string[]): number => + orders.findIndex((o) => sameOrder(o, order)); + /** The keys writes can add to a value of the shape, or null when a + * clone override can (the clone's order is its source's). */ + const movingKeys = (id: string, f: ShapeFacts, table: readonly string[]): string[] | null => { + const keys = new Set(table); + const moving = new Set(); + for (const w of f.writes) { + if (w.kind === "delete" || !keys.has(w.field) || !maybeAbsent(id, w.field)) continue; + if (w.kind === "clone") return null; + moving.add(w.field); + } + return [...moving]; + }; + const moves = new Map>(); + /** Adds to `orders` every order writes of `moving` keys reach from them + * and records each key's transitions; false past MAX_ORDERS. */ + const closeUnderMoves = (id: string, orders: string[][], moving: readonly string[]): boolean => { + const transitions = new Map(moving.map((k) => [k, []])); + for (let i = 0; i < orders.length; i++) { + for (const key of moving) { + const moved = [...orders[i]!.filter((k) => k !== key), key]; + let index = indexOfOrder(orders, moved); + if (index < 0) { + if (orders.length === MAX_ORDERS) return false; + index = orders.push(moved) - 1; + } + transitions.get(key)![i] = index; + } + } + if (moving.length > 0) moves.set(id, transitions); + return true; + }; + // Observed shapes, and the shapes their values copy their orders from. + const wanted = new Set(observed); + const pending = [...observed]; + while (pending.length > 0) { + const f = facts.get(pending.pop()!); + if (f === undefined) continue; + const sources = [ + ...f.copies.map((c) => c.source), + ...f.literals.flatMap((segments) => + segments.flatMap((s) => (s.kind === "spread" ? [s.shapeId] : [])), + ), + ]; + for (const source of sources) { + if (wanted.has(source)) continue; + wanted.add(source); + pending.push(source); + } + } + const orderable = ( + id: string, + f: ShapeFacts, + ): { shape: IrRecordShape; moving: string[] } | null => { + const shape = shapes.get(id); + if (!wanted.has(id) || !unresolved(id) || !inScope(shape)) return null; + const moving = movingKeys(id, f, tableOf(shape)); + return moving === null ? null : { shape, moving }; + }; + const variants = new Map(); + for (const [id, f] of facts) { + const candidate = orderable(id, f); + if (candidate === null || f.literals.length === 0) continue; + const table = tableOf(candidate.shape); + const orders: string[][] = [table]; + let ok = true; + const add = (order: readonly string[]): void => { + const full = complete(order, table); + if (indexOfOrder(orders, full) < 0) orders.push(full); + }; + for (const segments of f.literals) { + if (selfSpread(segments, id)) continue; + const order = literalOrder(segments); + if (order === null) ok = false; + else add(order); + } + for (const c of f.copies) { + if (c.source === id) continue; + const source = result.get(c.source); + if (source === undefined || source === "unknown") ok = false; + else add(source.order.filter((k) => c.keys.has(k))); + } + if (!ok || orders.length > MAX_ORDERS || !closeUnderMoves(id, orders, candidate.moving)) + continue; + if (orders.length > 1) variants.set(id, orders); + else moves.delete(id); + } + // Shapes built only from values of one shape with orders, all in one + // way: copies (rest bindings, width copies), or literals spreading the + // value between the same explicit keys. Order i is the source's order i + // over the shape's keys, after the keys before the spread and before + // the keys after it. + type Template = { source: string; prefix: string[]; suffix: string[] }; + const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => { + const prefix: string[] = []; + const suffix: string[] = []; + let source: string | null = null; + for (const segment of segments) { + if (segment.kind === "opaque") return null; + if (segment.kind === "spread") { + if (source !== null || segment.maybeAbsent || segment.shapeId === id) return null; + source = segment.shapeId; + continue; + } + if (!segment.definite) return null; + (source === null ? prefix : suffix).push(segment.name); + } + return source === null ? null : { source, prefix, suffix }; + }; + const templateKey = (t: Template): string => JSON.stringify([t.source, t.prefix, t.suffix]); + const copyTemplate = (source: string): Template => ({ source, prefix: [], suffix: [] }); + const mirrors = new Map(); + for (const [id, f] of facts) { + if (variants.has(id) || f.pinned || f.runtime || f.opaque) continue; + const candidate = orderable(id, f); + if (candidate === null || f.literals.length + f.copies.length === 0) continue; + const templates: (Template | null)[] = f.literals.map((segments) => templateOf(segments, id)); + for (const c of f.copies) templates.push(copyTemplate(c.source)); + const first = templates[0]; + if ( + first === null || + first === undefined || + templates.some((t) => t === null || templateKey(t) !== templateKey(first)) + ) + continue; + const orders = variants.get(first.source); + if (orders === undefined || mirrors.has(first.source)) continue; + const table = tableOf(candidate.shape); + const mirrored = orders.map((o) => { + const order = [...first.prefix]; + for (const k of [...o, ...first.suffix]) if (!order.includes(k)) order.push(k); + return complete(order, table); + }); + if (!closeUnderMoves(id, mirrored, candidate.moving)) continue; + if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) { + // Every order of the source agrees over these keys: one table. + moves.delete(id); + const shape = candidate.shape; + const only: string[] = mirrored[0] ?? table; + if (!sameOrder(only, table)) + shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...only]; + continue; + } + variants.set(id, mirrored); + mirrors.set(id, first); + } + if (variants.size === 0) return; + + // Every construction of those shapes, to give each its order index. A + // copy reads its source's index, so its source must be a plain binding. + const sites = new Map(); + /** The statements a spread literal's sequence evaluates first: the + * spread value's binding, then its fields. */ + const prologues = new Map(); + const siteOf = (e: IrExpr): boolean => { + if (e.kind === "seqExpr" && e.result.kind === "recordLit") prologues.set(e.result, e.stmts); + if (e.kind === "recordLit" && e.type.kind === "record" && variants.has(e.type.shapeId)) { + const list = sites.get(e.type.shapeId) ?? []; + list.push(e); + sites.set(e.type.shapeId, list); + } + return everyExprChild(e, siteOf, siteStmt); + }; + const siteStmt = (st: IrStmt): boolean => everyStmtChild(st, siteOf, siteStmt); + for (const fn of fns) for (const st of fn.body) siteStmt(st); + /** The binding a copy or spread reads its fields from. */ + const copySource = (lit: IrExpr & { kind: "recordLit" }, source: string): IrExpr | null => { + for (const field of lit.fields) { + const value = field.value; + if (value.kind === "recordGet" && value.shapeId === source && value.obj.kind === "varRef") + return value.obj; + } + for (const st of prologues.get(lit) ?? []) { + if (st.kind !== "varDecl" || st.init === null) continue; + const t = st.init.type; + if (t.kind === "record" && t.shapeId === source) + return { kind: "varRef", localId: st.localId, type: t, loc: lit.loc }; + } + return null; + }; + const orderIndex = (id: string, lit: IrExpr & { kind: "recordLit" }): IrExpr | number | null => { + const orders = variants.get(id)!; + const segments = notes.literalOf(lit); + if (segments !== undefined && selfSpread(segments, id)) { + const obj = copySource(lit, id); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: id, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + const mirror = mirrors.get(id); + if (segments !== undefined && mirror !== undefined) { + const obj = copySource(lit, mirror.source); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: mirror.source, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + if (segments !== undefined) { + const order = literalOrder(segments); + return order === null ? null : indexOfOrder(orders, complete(order, orders[0]!)); + } + const source = notes.copyOf(lit); + if (source === undefined) return 0; + if (variants.has(source) && (source === id || mirrors.get(id)?.source === source)) { + const obj = copySource(lit, source); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: source, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + const resolved = result.get(source); + if (resolved === undefined || resolved === "unknown") return null; + const keys = new Set( + lit.fields.filter((x) => x.value.kind !== "fieldAbsent").map((x) => x.name), + ); + return indexOfOrder( + orders, + complete( + resolved.order.filter((k) => keys.has(k)), + orders[0]!, + ), + ); + }; + const plan = new Map(); + const dropped = new Set(); + for (const [id, list] of sites) { + for (const lit of list) { + const index = orderIndex(id, lit); + if (index === null || (typeof index === "number" && index < 0)) dropped.add(id); + else plan.set(lit, index); + } + } + // Write sites of keys that move: each reads and updates the value's + // index before the write. + const writeSites: { st: IrStmt & { kind: "recordSet" }; fn: IrFunction }[] = []; + let writeFn: IrFunction | null = null; + const writeExpr = (e: IrExpr): boolean => everyExprChild(e, writeExpr, writeOf); + const writeOf = (st: IrStmt): boolean => { + if ( + st.kind === "recordSet" && + st.value.kind !== "fieldAbsent" && + moves.get(st.shapeId)?.has(st.field) + ) + writeSites.push({ st, fn: writeFn! }); + return everyStmtChild(st, writeExpr, writeOf); + }; + for (const fn of fns) { + writeFn = fn; + for (const st of fn.body) writeOf(st); + } + // A shape whose copies cannot read their source keeps today's table, and + // so do the shapes that mirror it. + for (const [id, mirror] of mirrors) if (dropped.has(mirror.source)) dropped.add(id); + for (const [lit, index] of plan) { + const id = (lit.type as IrType & { kind: "record" }).shapeId; + if (dropped.has(id)) continue; + const value: IrExpr = + typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index; + lit.fields.push({ name: KEY_ORDER_FIELD, value }); + } + let receivers = 0; + const replacements = new Map>(); + for (const { st, fn } of writeSites) { + if (dropped.has(st.shapeId)) continue; + const transitions = moves.get(st.shapeId)!.get(st.field)!; + const loc = st.loc; + // A receiver other than a binding evaluates once, into a local. + let plain = st.obj; + while (plain.kind === "unionNarrow" || plain.kind === "downcast") plain = plain.value; + const prologue: IrStmt[] = []; + let obj = st.obj; + if (plain.kind !== "varRef") { + const localId = `%keyOrderReceiver.${receivers++}`; + fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false }); + prologue.push({ kind: "varDecl", localId, init: st.obj, loc }); + obj = { kind: "varRef", localId, type: st.obj.type, loc }; + } + const current: IrExpr = { + kind: "recordGet", + obj, + shapeId: st.shapeId, + field: KEY_ORDER_FIELD, + type: F64, + loc, + }; + // transitions[i] for the current index i, as a chain of tests. + let next: IrExpr = { + kind: "numLit", + value: transitions[transitions.length - 1]!, + type: F64, + loc, + }; + for (let i = transitions.length - 2; i >= 0; i--) { + next = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: current, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: transitions[i]!, type: F64, loc }, + else_: next, + type: F64, + loc, + }; + } + const absent: IrExpr = { + kind: "unary", + op: "!", + operand: lowerer.recordFieldPresent(obj, st.shapeId, st.field, loc), + type: BOOL, + loc, + }; + const update: IrStmt = { + kind: "recordSet", + obj, + shapeId: st.shapeId, + field: KEY_ORDER_FIELD, + value: next, + loc, + }; + // The write becomes a block that moves the key first. + const write: IrStmt = { + kind: "recordSet", + obj, + shapeId: st.shapeId, + field: st.field, + value: st.value, + loc, + }; + const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc }; + const block: IrStmt = { kind: "block", body: [...prologue, guard, write], loc }; + let replaced = replacements.get(fn); + if (replaced === undefined) { + replaced = new Map(); + replacements.set(fn, replaced); + } + replaced.set(st, block); + } + for (const [fn, replaced] of replacements) { + fn.body = transformStmtList(fn.body, { + expr: (e) => e, + stmt: (st) => replaced.get(st) ?? st, + }); + } + for (const [id, orders] of variants) { + if (dropped.has(id)) continue; + const shape = shapes.get(id)!; + const indexKeys: string[] = (shape.declaredOrder ?? []).filter(isArrayIndex); + shapes.addKeyOrderField(id); + const tables: string[][] = []; + for (const order of orders) { + const table: string[] = [...indexKeys]; + for (const key of order) table.push(key); + tables.push(table); + } + shape.orderVariants = tables; + } } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index bd5068d05..0d45628ed 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -71,6 +72,7 @@ import { isDynTypedRefType, isUnitType, shapeHasAccessorSlots, + shapeKeyOrders, typeEquals, RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; @@ -7029,8 +7031,12 @@ export function recordKeysArrayCall( loc, }, ]; - const order = current.declaredOrder ?? current.fields.map((f) => f.name); - for (const name of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, name] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const f = current.fields.find((x) => x.name === name)!; const pushStmt: IrStmt = { kind: "exprStmt", @@ -7047,12 +7053,13 @@ export function recordKeysArrayCall( // Fields that can be absent push behind their presence test (the // key exists exactly when Object.keys would list it). const present = lowerer.recordFieldPresent(ref, argIr.shapeId, f.name, loc); - body.push( + walks[k]!.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [pushStmt], else_: null, loc } : pushStmt, ); } + body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; @@ -8709,8 +8716,12 @@ function lowerObjectStaticCall( loc, }, ]; - const order = current.declaredOrder ?? current.fields.map((f) => f.name); - for (const name of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, name] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const f = current.fields.find((x) => x.name === name)!; const raw: IrExpr = { kind: "recordGet", @@ -8872,8 +8883,9 @@ function lowerObjectStaticCall( loc, }; } - body.push(stmt); + walks[k]!.push(stmt); } + body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 47f490c7b..57f16bcc7 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { perKeyOrder } from "./key-order-witness.js"; import { dynUndefinedExpr, boolLit, @@ -40,6 +41,7 @@ import { isDynTypedRefType, recordTextCodecClass, shapeHasAccessorSlots, + shapeKeyOrders, typeKey, } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; @@ -1647,7 +1649,12 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { }); } body.push(depthGate("[Object]"), ...begin()); - for (const fname of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, fname] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const ft = byName.get(fname); if (!ft) continue; const show = entry( @@ -1655,12 +1662,13 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { boolLit(false, loc), ); const present = lowerer.recordFieldPresent(v(), t.shapeId, fname, loc); - body.push( + walks[k]!.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [show], else_: null, loc } : show, ); } + body.push(...perKeyOrder(current, v(), walks, loc)); body.push( ret(end(strLit("", loc), strLit("{", loc), strLit("}", loc), false, boolLit(false, loc))), ); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 9abd5a0b0..ee942c032 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -42,7 +42,7 @@ import { UNDEFINED_T, VOID, } from "../ir/ir.js"; -import { BIGINT_T } from "../ir/ir.js"; +import { BIGINT_T, KEY_ORDER_FIELD } from "../ir/ir.js"; import { literalValues } from "./literal-values.js"; import { remapUnionDiscriminant } from "./union-discriminants.js"; @@ -523,6 +523,21 @@ export class ShapeRegistry { return missing.length === 0 ? order : [...order, ...missing]; } + /** Gives a shape with orderVariants its KEY_ORDER_FIELD, in place, + * after the bodies lowered (key-order-witness.ts adds the field to each + * of the shape's literals). Allocation zeroes it, which every other + * construction keeps. */ + addKeyOrderField(shapeId: string): void { + const shape = this.byId.get(shapeId); + if (!shape || shape.fields.some((f) => f.name === KEY_ORDER_FIELD)) return; + shape.fields = [...shape.fields, { name: KEY_ORDER_FIELD, type: F64 }].sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0, + ); + this.revision++; + const key = this.keyOf(shape.fields, false, shape.indexValue); + if (!this.byKey.has(key)) this.byKey.set(key, shapeId); + } + /** Adds optional fields to an interned shape in place: members a type * assertion attaches to the shape's values (`r as R & { tag: string }` * followed by `view.tag = ...`; see assertion-members.ts). Every value diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 7bef40aa6..0a802c03d 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -172,6 +172,7 @@ const MODULE_FIELDS: Record>, Role> = { "records[].fields[].name": "other", "records[].declaredOrder[]": "other", "records[].accessorOrder[]": "other", + "records[].orderVariants[][]": "other", "unions[].id": "other", "unions[].discriminant.field": "other", "unions[].discriminant.cases[].values[]": "other", diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4a395feb2..89a5ee73a 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1665,6 +1665,24 @@ export interface IrRecordShape { * an accessor property named once (see accessorSlotProp). The * checked-dynamic copy lists its keys in this order. */ accessorOrder?: string[]; + /** Every order the shape's values enumerate their keys in, when its + * object literals write the keys in more than one order (see + * key-order-witness.ts); declaredOrder is the first. Each value holds the + * index of its order in the hidden number field KEY_ORDER_FIELD: the + * literal that wrote it stores its order's index, a copy its source's, + * and every other construction leaves 0. Each key-order surface + * dispatches on that field. */ + orderVariants?: string[][]; +} + +/** The hidden field of a shape with orderVariants: the index of the value's + * key order. */ +export const KEY_ORDER_FIELD = "%order"; + +/** The orders a record shape's values enumerate their keys in: each order + * of a shape with orderVariants, or its one table. */ +export function shapeKeyOrders(shape: IrRecordShape): readonly (readonly string[])[] { + return shape.orderVariants ?? [shape.declaredOrder ?? shape.fields.map((f) => f.name)]; } /** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json new file mode 100644 index 000000000..3551ad6b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7122-key-order-json.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..4451e935a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7123-key-order-keys-values-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json new file mode 100644 index 000000000..e87101734 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7124-key-order-for-in.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json new file mode 100644 index 000000000..b13a13261 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7125-key-order-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json new file mode 100644 index 000000000..a9f55663d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7126-key-order-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json new file mode 100644 index 000000000..5a091d157 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7127-key-order-dynamic.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json new file mode 100644 index 000000000..407a2179e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7128-key-order-late-writes.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7122-key-order-json.ts b/tests/corpus/7122-key-order-json.ts new file mode 100644 index 000000000..0cd3dff61 --- /dev/null +++ b/tests/corpus/7122-key-order-json.ts @@ -0,0 +1,31 @@ +// JSON.stringify of object literals that write the same keys in different +// orders: each value serializes in the order its literal wrote, as in Node. +type Point = { x: number; y: number; label?: string }; + +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) console.log(JSON.stringify(value)); +console.log(JSON.stringify([a, b, c])); +console.log(JSON.stringify({ first: b, rest: [c, a] }, null, 2)); + +// Optional keys the literal leaves out, and writes to keys it has. +const p: Point = { y: 2, x: 1 }; +const q: Point = { label: "q", x: 3, y: 4 }; +const r: Point = { x: 5, y: 6, label: "r" }; +q.x = 30; +r.label = "r2"; +for (const point of [p, q, r]) console.log(JSON.stringify(point)); + +// A function typed by one literal's shape receiving the others. +function describe(value: { id: number; name: string; ok: boolean }): string { + return JSON.stringify(value); +} +console.log(describe(c), describe(a)); + +// JSON.parse into the same record type keeps each text's own key order. +type Entry = { id: number; name: string; ok: boolean }; +for (const text of ['{"ok":true,"name":"p","id":7}', '{"id":8,"name":"q","ok":false}']) { + const parsed = JSON.parse(text) as Entry; + console.log(JSON.stringify(parsed), Object.keys(parsed).join(","), describe(parsed)); +} diff --git a/tests/corpus/7123-key-order-keys-values-entries.ts b/tests/corpus/7123-key-order-keys-values-entries.ts new file mode 100644 index 000000000..29f725c24 --- /dev/null +++ b/tests/corpus/7123-key-order-keys-values-entries.ts @@ -0,0 +1,19 @@ +// Object.keys, Object.values, Object.entries and Object.fromEntries over +// object literals that write the same keys in different orders. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + console.log(Object.keys(value).join(",")); + console.log(Object.values(value).join(",")); + console.log(Object.entries(value).map(([key, item]) => `${key}=${String(item)}`).join(" ")); + console.log(JSON.stringify(Object.fromEntries(Object.entries(value)))); +} + +type Row = { year: number; title?: string; score: number }; +const rows: Row[] = [ + { score: 1, year: 2001 }, + { title: "t", year: 2002, score: 2 }, + { year: 2003, score: 3, title: "u" }, +]; +for (const row of rows) console.log(Object.keys(row).join(","), Object.values(row).length); diff --git a/tests/corpus/7124-key-order-for-in.ts b/tests/corpus/7124-key-order-for-in.ts new file mode 100644 index 000000000..89ed16edb --- /dev/null +++ b/tests/corpus/7124-key-order-for-in.ts @@ -0,0 +1,15 @@ +// for-in over object literals that write the same keys in different +// orders, and over values a function received. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +function keysOf(value: { id: number; name: string; ok: boolean }): string { + const keys: string[] = []; + for (const key in value) keys.push(key); + return keys.join(","); +} +for (const value of [a, b, c]) { + const seen: string[] = []; + for (const key in value) seen.push(key.toUpperCase()); + console.log(seen.join(" "), keysOf(value)); +} diff --git a/tests/corpus/7125-key-order-inspect.ts b/tests/corpus/7125-key-order-inspect.ts new file mode 100644 index 000000000..6a11eaf2c --- /dev/null +++ b/tests/corpus/7125-key-order-inspect.ts @@ -0,0 +1,12 @@ +import { inspect } from "node:util"; + +// console.log and util.inspect of object literals that write the same keys +// in different orders, alone and nested. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +console.log(a, b, c); +console.log([a, b, c]); +console.log({ outer: { inner: b }, list: [c] }); +console.log(inspect(b, { depth: 0 }), inspect({ deep: { deeper: c } }, { depth: 0 })); +console.log(`${inspect(c)} / ${inspect(a)}`); diff --git a/tests/corpus/7126-key-order-copies.ts b/tests/corpus/7126-key-order-copies.ts new file mode 100644 index 000000000..daafba23b --- /dev/null +++ b/tests/corpus/7126-key-order-copies.ts @@ -0,0 +1,19 @@ +// Copies of object literals that write the same keys in different orders: +// spreads, spreads with other keys before and after, rest bindings, +// Object.assign and structuredClone keep each value's own order. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + const copy = { ...value }; + const changed = { ...value, id: 10 }; + const after = { ...value, extra: "e" }; + const before = { first: 0, ...value }; + const { name, ...rest } = value; + const assigned = Object.assign({}, value); + const cloned = structuredClone(value); + console.log(JSON.stringify(copy), JSON.stringify(changed)); + console.log(JSON.stringify(after), Object.keys(before).join(",")); + console.log(name, JSON.stringify(rest), Object.keys(rest).join(",")); + console.log(JSON.stringify(assigned), JSON.stringify(cloned)); +} diff --git a/tests/corpus/7127-key-order-dynamic.ts b/tests/corpus/7127-key-order-dynamic.ts new file mode 100644 index 000000000..55970cb40 --- /dev/null +++ b/tests/corpus/7127-key-order-dynamic.ts @@ -0,0 +1,30 @@ +import { MessageChannel } from "node:worker_threads"; + +// Object literals that write the same keys in different orders, seen +// through untyped values: their keys, JSON text, own property names, and a +// MessageChannel message. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +const values: unknown[] = [a, b, c]; +for (const value of values) { + const object = value as Record; + console.log(Object.keys(object).join(","), JSON.stringify(object)); + console.log(Object.getOwnPropertyNames(object).join(",")); +} +const { port1, port2 } = new MessageChannel(); +let received = 0; +port1.on("message", (message: unknown) => { + console.log("message", JSON.stringify(message), Object.keys(message as object).join(",")); + if (++received === 3) port1.close(); +}); +for (const value of [a, b, c]) port2.postMessage(value); + +// A nested value seen through an untyped view keeps its identity and its +// own key order, and writes through the view reach it. +const holder = { inner: b, tag: "h" }; +const view = holder as unknown as Record; +const inner = view.inner as Record; +console.log(view.inner === view.inner, Object.keys(inner).join(",")); +inner.id = 20; +console.log(b.id, JSON.stringify(b), JSON.stringify(view)); diff --git a/tests/corpus/7128-key-order-late-writes.ts b/tests/corpus/7128-key-order-late-writes.ts new file mode 100644 index 000000000..b1afd8761 --- /dev/null +++ b/tests/corpus/7128-key-order-late-writes.ts @@ -0,0 +1,27 @@ +// Optional keys added after construction go last, as in Node, also for +// shapes whose literals write their keys in different orders: a write of a +// missing key, a delete and a later write, and writes in a loop. +type Item = { id: number; name?: string; note?: string; count: number }; + +const items: Item[] = [ + { id: 1, count: 0 }, + { count: 1, name: "b", id: 2 }, + { note: "c", id: 3, count: 2 }, +]; +items[0]!.note = "added"; +items[0]!.name = "later"; +items[1]!.name = "kept"; +delete items[2]!.note; +items[2]!.note = "again"; +for (const item of items) { + if (item.count > 0) item.name ??= "filled"; + console.log(JSON.stringify(item), Object.keys(item).join(",")); +} +const built: Item = { count: 9, id: 9 }; +for (let i = 0; i < 2; i++) { + if (i === 1) built.note = `n${i}`; + console.log(Object.keys(built).join(",")); +} +console.log(built); +// d +// e diff --git a/tests/harness/corpus-costs/7122-key-order-json.ts.json b/tests/harness/corpus-costs/7122-key-order-json.ts.json new file mode 100644 index 000000000..aa8658007 --- /dev/null +++ b/tests/harness/corpus-costs/7122-key-order-json.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/corpus-costs/7123-key-order-keys-values-entries.ts.json b/tests/harness/corpus-costs/7123-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..58d7b29f8 --- /dev/null +++ b/tests/harness/corpus-costs/7123-key-order-keys-values-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 2800 } diff --git a/tests/harness/corpus-costs/7124-key-order-for-in.ts.json b/tests/harness/corpus-costs/7124-key-order-for-in.ts.json new file mode 100644 index 000000000..e0b1e4dd1 --- /dev/null +++ b/tests/harness/corpus-costs/7124-key-order-for-in.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2100 } diff --git a/tests/harness/corpus-costs/7125-key-order-inspect.ts.json b/tests/harness/corpus-costs/7125-key-order-inspect.ts.json new file mode 100644 index 000000000..932ea4809 --- /dev/null +++ b/tests/harness/corpus-costs/7125-key-order-inspect.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2500 } diff --git a/tests/harness/corpus-costs/7126-key-order-copies.ts.json b/tests/harness/corpus-costs/7126-key-order-copies.ts.json new file mode 100644 index 000000000..ba3e270ee --- /dev/null +++ b/tests/harness/corpus-costs/7126-key-order-copies.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2400 } diff --git a/tests/harness/corpus-costs/7127-key-order-dynamic.ts.json b/tests/harness/corpus-costs/7127-key-order-dynamic.ts.json new file mode 100644 index 000000000..12680c6af --- /dev/null +++ b/tests/harness/corpus-costs/7127-key-order-dynamic.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2700 } diff --git a/tests/harness/corpus-costs/7128-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7128-key-order-late-writes.ts.json new file mode 100644 index 000000000..0b945a1c9 --- /dev/null +++ b/tests/harness/corpus-costs/7128-key-order-late-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2500 } From ce47b9bad95fd3284f151cd5c04d0ecc9dea881a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:54:53 -0500 Subject: [PATCH 008/169] Number the key-order corpus programs 7132 to 7138 Corpus 7120 to 7129 are taken on main. --- .../order-parity/tests/corpus/7122-key-order-json.ts.json | 6 ------ .../tests/corpus/7123-key-order-keys-values-entries.ts.json | 6 ------ .../order-parity/tests/corpus/7124-key-order-for-in.ts.json | 6 ------ .../tests/corpus/7125-key-order-inspect.ts.json | 6 ------ .../order-parity/tests/corpus/7126-key-order-copies.ts.json | 6 ------ .../tests/corpus/7127-key-order-dynamic.ts.json | 6 ------ .../tests/corpus/7128-key-order-late-writes.ts.json | 6 ------ .../order-parity/tests/corpus/7132-key-order-json.ts.json | 6 ++++++ .../tests/corpus/7133-key-order-keys-values-entries.ts.json | 6 ++++++ .../order-parity/tests/corpus/7134-key-order-for-in.ts.json | 6 ++++++ .../tests/corpus/7135-key-order-inspect.ts.json | 6 ++++++ .../order-parity/tests/corpus/7136-key-order-copies.ts.json | 6 ++++++ .../tests/corpus/7137-key-order-dynamic.ts.json | 6 ++++++ .../tests/corpus/7138-key-order-late-writes.ts.json | 6 ++++++ .../{7122-key-order-json.ts => 7132-key-order-json.ts} | 0 ...ues-entries.ts => 7133-key-order-keys-values-entries.ts} | 0 .../{7124-key-order-for-in.ts => 7134-key-order-for-in.ts} | 0 ...{7125-key-order-inspect.ts => 7135-key-order-inspect.ts} | 0 .../{7126-key-order-copies.ts => 7136-key-order-copies.ts} | 0 ...{7127-key-order-dynamic.ts => 7137-key-order-dynamic.ts} | 0 ...y-order-late-writes.ts => 7138-key-order-late-writes.ts} | 0 ...2-key-order-json.ts.json => 7132-key-order-json.ts.json} | 0 ...s.ts.json => 7133-key-order-keys-values-entries.ts.json} | 0 ...y-order-for-in.ts.json => 7134-key-order-for-in.ts.json} | 0 ...order-inspect.ts.json => 7135-key-order-inspect.ts.json} | 0 ...y-order-copies.ts.json => 7136-key-order-copies.ts.json} | 0 ...order-dynamic.ts.json => 7137-key-order-dynamic.ts.json} | 0 ...te-writes.ts.json => 7138-key-order-late-writes.ts.json} | 0 28 files changed, 42 insertions(+), 42 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json rename tests/corpus/{7122-key-order-json.ts => 7132-key-order-json.ts} (100%) rename tests/corpus/{7123-key-order-keys-values-entries.ts => 7133-key-order-keys-values-entries.ts} (100%) rename tests/corpus/{7124-key-order-for-in.ts => 7134-key-order-for-in.ts} (100%) rename tests/corpus/{7125-key-order-inspect.ts => 7135-key-order-inspect.ts} (100%) rename tests/corpus/{7126-key-order-copies.ts => 7136-key-order-copies.ts} (100%) rename tests/corpus/{7127-key-order-dynamic.ts => 7137-key-order-dynamic.ts} (100%) rename tests/corpus/{7128-key-order-late-writes.ts => 7138-key-order-late-writes.ts} (100%) rename tests/harness/corpus-costs/{7122-key-order-json.ts.json => 7132-key-order-json.ts.json} (100%) rename tests/harness/corpus-costs/{7123-key-order-keys-values-entries.ts.json => 7133-key-order-keys-values-entries.ts.json} (100%) rename tests/harness/corpus-costs/{7124-key-order-for-in.ts.json => 7134-key-order-for-in.ts.json} (100%) rename tests/harness/corpus-costs/{7125-key-order-inspect.ts.json => 7135-key-order-inspect.ts.json} (100%) rename tests/harness/corpus-costs/{7126-key-order-copies.ts.json => 7136-key-order-copies.ts.json} (100%) rename tests/harness/corpus-costs/{7127-key-order-dynamic.ts.json => 7137-key-order-dynamic.ts.json} (100%) rename tests/harness/corpus-costs/{7128-key-order-late-writes.ts.json => 7138-key-order-late-writes.ts.json} (100%) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json deleted file mode 100644 index 3551ad6b1..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-key-order-json.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7122-key-order-json.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json deleted file mode 100644 index 4451e935a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-key-order-keys-values-entries.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7123-key-order-keys-values-entries.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json deleted file mode 100644 index e87101734..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-key-order-for-in.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7124-key-order-for-in.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json deleted file mode 100644 index b13a13261..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-key-order-inspect.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7125-key-order-inspect.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json deleted file mode 100644 index a9f55663d..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-key-order-copies.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7126-key-order-copies.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json deleted file mode 100644 index 5a091d157..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-key-order-dynamic.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7127-key-order-dynamic.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json deleted file mode 100644 index 407a2179e..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-key-order-late-writes.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7128-key-order-late-writes.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json new file mode 100644 index 000000000..2609ff929 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7132-key-order-json.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..40729240a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7133-key-order-keys-values-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json new file mode 100644 index 000000000..e44503626 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7134-key-order-for-in.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json new file mode 100644 index 000000000..431bf5c9c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7135-key-order-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json new file mode 100644 index 000000000..fea64a145 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7136-key-order-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json new file mode 100644 index 000000000..3b7cab36f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7137-key-order-dynamic.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json new file mode 100644 index 000000000..7f5302cd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7138-key-order-late-writes.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7122-key-order-json.ts b/tests/corpus/7132-key-order-json.ts similarity index 100% rename from tests/corpus/7122-key-order-json.ts rename to tests/corpus/7132-key-order-json.ts diff --git a/tests/corpus/7123-key-order-keys-values-entries.ts b/tests/corpus/7133-key-order-keys-values-entries.ts similarity index 100% rename from tests/corpus/7123-key-order-keys-values-entries.ts rename to tests/corpus/7133-key-order-keys-values-entries.ts diff --git a/tests/corpus/7124-key-order-for-in.ts b/tests/corpus/7134-key-order-for-in.ts similarity index 100% rename from tests/corpus/7124-key-order-for-in.ts rename to tests/corpus/7134-key-order-for-in.ts diff --git a/tests/corpus/7125-key-order-inspect.ts b/tests/corpus/7135-key-order-inspect.ts similarity index 100% rename from tests/corpus/7125-key-order-inspect.ts rename to tests/corpus/7135-key-order-inspect.ts diff --git a/tests/corpus/7126-key-order-copies.ts b/tests/corpus/7136-key-order-copies.ts similarity index 100% rename from tests/corpus/7126-key-order-copies.ts rename to tests/corpus/7136-key-order-copies.ts diff --git a/tests/corpus/7127-key-order-dynamic.ts b/tests/corpus/7137-key-order-dynamic.ts similarity index 100% rename from tests/corpus/7127-key-order-dynamic.ts rename to tests/corpus/7137-key-order-dynamic.ts diff --git a/tests/corpus/7128-key-order-late-writes.ts b/tests/corpus/7138-key-order-late-writes.ts similarity index 100% rename from tests/corpus/7128-key-order-late-writes.ts rename to tests/corpus/7138-key-order-late-writes.ts diff --git a/tests/harness/corpus-costs/7122-key-order-json.ts.json b/tests/harness/corpus-costs/7132-key-order-json.ts.json similarity index 100% rename from tests/harness/corpus-costs/7122-key-order-json.ts.json rename to tests/harness/corpus-costs/7132-key-order-json.ts.json diff --git a/tests/harness/corpus-costs/7123-key-order-keys-values-entries.ts.json b/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json similarity index 100% rename from tests/harness/corpus-costs/7123-key-order-keys-values-entries.ts.json rename to tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json diff --git a/tests/harness/corpus-costs/7124-key-order-for-in.ts.json b/tests/harness/corpus-costs/7134-key-order-for-in.ts.json similarity index 100% rename from tests/harness/corpus-costs/7124-key-order-for-in.ts.json rename to tests/harness/corpus-costs/7134-key-order-for-in.ts.json diff --git a/tests/harness/corpus-costs/7125-key-order-inspect.ts.json b/tests/harness/corpus-costs/7135-key-order-inspect.ts.json similarity index 100% rename from tests/harness/corpus-costs/7125-key-order-inspect.ts.json rename to tests/harness/corpus-costs/7135-key-order-inspect.ts.json diff --git a/tests/harness/corpus-costs/7126-key-order-copies.ts.json b/tests/harness/corpus-costs/7136-key-order-copies.ts.json similarity index 100% rename from tests/harness/corpus-costs/7126-key-order-copies.ts.json rename to tests/harness/corpus-costs/7136-key-order-copies.ts.json diff --git a/tests/harness/corpus-costs/7127-key-order-dynamic.ts.json b/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json similarity index 100% rename from tests/harness/corpus-costs/7127-key-order-dynamic.ts.json rename to tests/harness/corpus-costs/7137-key-order-dynamic.ts.json diff --git a/tests/harness/corpus-costs/7128-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json similarity index 100% rename from tests/harness/corpus-costs/7128-key-order-late-writes.ts.json rename to tests/harness/corpus-costs/7138-key-order-late-writes.ts.json From 4cb46d6472af2f82aa2161c0ae6bccf438389449 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:39:06 -0500 Subject: [PATCH 009/169] Keep a key-order divergence in the coverage fixture The fixture's Point literals now each record their own key order, so they no longer diverge. The fixture adds a spread that sets an optional key its source lacks, which keeps the type's declaration order, so SC6002 still reports the literal written in another order. --- tests/coverage-fixtures/divergences.ts | 10 +++++++--- tests/harness/__snapshots__/coverage-divergences.txt | 10 +++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index 83c3ede49..0cf8bcae5 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,18 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: values of one type are built in two orders and the program -// observes them, so the type keeps its declaration order. +// Key order: values of one type are built in two orders, and a spread sets +// an optional key its source lacks, so the values cannot record their own +// orders and the type keeps its declaration order. interface Point { x: number; y: number; + label?: string; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); +const other: Point = { x: 3, y: 4 }; +const labeled: Point = { ...point, label: "p" }; +console.log(JSON.stringify(point), JSON.stringify(other), JSON.stringify(labeled)); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/harness/__snapshots__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt index 66373655f..8ab286873 100644 --- a/tests/harness/__snapshots__/coverage-divergences.txt +++ b/tests/harness/__snapshots__/coverage-divergences.txt @@ -1,7 +1,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts - statements analyzed 18 - compile statically 18 (100%) + statements analyzed 20 + compile statically 20 (100%) in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) fully static — all analyzed statements compile to native code. @@ -14,11 +14,11 @@ scriptc coverage tests/coverage-fixtures/divergences.ts at tests/coverage-fixtures/divergences.ts:20:23 hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class ×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in … - at tests/coverage-fixtures/divergences.ts:29:22 + at tests/coverage-fixtures/divergences.ts:31:22 hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built) ×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation - at tests/coverage-fixtures/divergences.ts:33:70 + at tests/coverage-fixtures/divergences.ts:37:70 hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree ×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally - at tests/coverage-fixtures/divergences.ts:48:20 + at tests/coverage-fixtures/divergences.ts:52:20 hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share \ No newline at end of file From 41ef12e4633e678272bb07eef8f2cee5af9f2242 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:44:59 -0500 Subject: [PATCH 010/169] Construct builtins through stored constructors and fix function-local class names and dispatch --- .../limitations/builtin-apis/prototypes.mdx | 4 +- .../local-class-expressions.mdx | 3 +- .../lowering/class-runtime-statics.ts | 38 +++- .../lowering/expressions/identifiers.ts | 3 +- .../lowering/expressions/object-literals.ts | 3 + .../src/frontend/lowering/lower-classes.ts | 173 +++++++++++++++++- .../src/frontend/lowering/lower-stmts.ts | 8 + .../compiler/src/frontend/lowering/lowerer.ts | 7 +- .../src/frontend/lowering/surfaces.ts | 13 +- ...6913-builtin-constructor-shorthand.ts.json | 6 + .../6914-dynamic-builtin-construct.ts.json | 6 + .../6915-dynamic-builtin-construct.mjs.json | 6 + ...local-class-static-self-reference.mjs.json | 6 + ...al-class-expression-self-reference.ts.json | 6 + ...l-class-expression-self-reference.mjs.json | 6 + .../6919-local-class-inspect-name.mjs.json | 6 + ...1-local-subclass-override-dispatch.ts.json | 6 + ...-local-subclass-override-dispatch.mjs.json | 6 + packages/runtime/src/scr_dyn.h | 9 + packages/runtime/src/scr_dyn_invoke.c | 32 ++++ packages/runtime/src/scr_json.c | 33 ++++ packages/runtime/src/scr_map.c | 2 +- packages/runtime/src/scr_map.h | 4 +- .../6913-builtin-constructor-shorthand.ts | 10 + .../corpus/6914-dynamic-builtin-construct.ts | 54 ++++++ .../corpus/6915-dynamic-builtin-construct.mjs | 16 ++ ...6916-local-class-static-self-reference.mjs | 52 ++++++ ...7-local-class-expression-self-reference.ts | 41 +++++ ...-local-class-expression-self-reference.mjs | 50 +++++ .../corpus/6919-local-class-inspect-name.mjs | 51 ++++++ .../6921-local-subclass-override-dispatch.ts | 51 ++++++ .../6922-local-subclass-override-dispatch.mjs | 51 ++++++ tests/diagnostics/local-class-boundaries.ts | 2 - .../local-class-boundaries.ts.txt | 17 +- ...6913-builtin-constructor-shorthand.ts.json | 1 + .../6914-dynamic-builtin-construct.ts.json | 1 + .../6915-dynamic-builtin-construct.mjs.json | 1 + ...local-class-static-self-reference.mjs.json | 1 + ...al-class-expression-self-reference.ts.json | 1 + ...l-class-expression-self-reference.mjs.json | 1 + .../6919-local-class-inspect-name.mjs.json | 1 + ...1-local-subclass-override-dispatch.ts.json | 1 + ...-local-subclass-override-dispatch.mjs.json | 1 + 43 files changed, 758 insertions(+), 32 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json create mode 100644 tests/corpus/6913-builtin-constructor-shorthand.ts create mode 100644 tests/corpus/6914-dynamic-builtin-construct.ts create mode 100644 tests/corpus/6915-dynamic-builtin-construct.mjs create mode 100644 tests/corpus/6916-local-class-static-self-reference.mjs create mode 100644 tests/corpus/6917-local-class-expression-self-reference.ts create mode 100644 tests/corpus/6918-local-class-expression-self-reference.mjs create mode 100644 tests/corpus/6919-local-class-inspect-name.mjs create mode 100644 tests/corpus/6921-local-subclass-override-dispatch.ts create mode 100644 tests/corpus/6922-local-subclass-override-dispatch.mjs create mode 100644 tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json create mode 100644 tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json create mode 100644 tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json create mode 100644 tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json create mode 100644 tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json create mode 100644 tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json create mode 100644 tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json create mode 100644 tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json create mode 100644 tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 3e0b5789d..170b11793 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -3,4 +3,6 @@ Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly. Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not. `Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported. - The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported. + The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called. + `new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression. + `new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported. diff --git a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx index 173bdc0c6..de24038cc 100644 --- a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx +++ b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx @@ -1,5 +1,6 @@ -- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported. +- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, and generic members are unsupported. Static methods read the enclosing function's bindings and are inherited by subclasses. + A named class expression's methods and static members can refer to the class by its name. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. diff --git a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts index 310c8f389..707c916ec 100644 --- a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts +++ b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts @@ -1,6 +1,6 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; -import { BOOL, DYN, STRING, type IrExpr, type IrStmt } from "../../ir/ir.js"; +import { BOOL, DYN, STRING, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import { locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import { classValueRef, type ClassInfo } from "./lower-classes.js"; @@ -15,9 +15,29 @@ export function hasRuntimeStatics(info: ClassInfo): boolean { /** Constructor descriptors preserve inherited getter receivers and static * initialization order. Factory evaluations additionally own independent - * fields and closures when the same factory is called repeatedly. */ -export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, value: IrExpr): IrExpr { - if (!info.runtimeStatics && !info.runtimePrototypeMembers) return value; + * fields and closures when the same factory is called repeatedly. + * + * `binding` is the local that holds the class under its own name (a + * function-local class declaration's binding, or the inner name of a + * function-local class expression whose body names itself), declared + * without a value. It is assigned before any static member is defined, so + * static initializers and methods read the class through it. */ +export function initializeRuntimeStatics( + lowerer: Lowerer, + info: ClassInfo, + value: IrExpr, + binding?: IrLocal, +): IrExpr { + if (!info.runtimeStatics && !info.runtimePrototypeMembers) { + if (!binding) return value; + return { + kind: "seqExpr", + stmts: [{ kind: "assign", localId: binding.id, value, loc: value.loc }], + result: varRef(binding.id, binding.type, value.loc), + type: binding.type, + loc: value.loc, + }; + } const loc = value.loc; const local = lowerer.declareHiddenLocal("%staticClass", value.type); const receiver = varRef(local.id, value.type, loc); @@ -28,6 +48,16 @@ export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, valu : dynUndefinedExpr(loc); const statements: IrStmt[] = [ { kind: "varDecl", localId: local.id, init: value, loc }, + ...(binding + ? [ + { + kind: "assign" as const, + localId: binding.id, + value: lowerer.coerceToExpected(receiver, binding.type), + loc, + }, + ] + : []), { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, base], type: DYN, loc }, diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 1f74cea42..63175d50a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -69,6 +69,7 @@ import { builtinModuleConstOf, builtinModuleFnOf, builtinModulesArrayLit, + identifierReadSymbol, stdlibGlobalNameOf, } from "../surfaces.js"; import { builtinConstantsObject } from "../lower-builtin-values.js"; @@ -753,7 +754,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return lowerGlobalValue(lowerer, expr); } { - const sym = lowerer.checker.getSymbolAtLocation(expr); + const sym = identifierReadSymbol(lowerer, expr); // The temporal global is typed (TS 7's lib slice) but Node 24 does // not define it: the read throws Node's ReferenceError. if (isTemporalGlobalRoot(lowerer, expr, sym)) { diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 678b375fe..155529920 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -3262,6 +3262,9 @@ export function lowerShorthandValue( } return lowerer.classValueRef(classInfo, propName); } + // A standard-library global (`{ Date }`): the value the long form + // `{ Date: Date }` reads. + if (lowerer.isStdlibSymbol(resolved)) return lowerer.lowerExpr(propName); } lowerer.rejectUnresolvedSymbol( symbol diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..c74969d10 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -6034,6 +6034,31 @@ export function lowerClassExpressionInfo( return info; } +/** True when a named class expression's body refers to the class value by + * its own name (`class Box { static of() { return new Box(); } }`); type + * annotations naming it (`again(): Box`) do not count. */ +function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): boolean { + const name = expr.name; + if (!name) return false; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (!symbol) return false; + let found = false; + for (const member of expr.members.nodes) { + ts.walkPreorder(member, (n) => { + if (found || ts.isTypeNode(n)) return "skip"; + if ( + ts.isIdentifier(n) && + n.text === name.text && + lowerer.checker.getSymbolAtLocation(n) === symbol + ) + found = true; + return undefined; + }); + if (found) return true; + } + return false; +} + /** `class {…}` in expression position: a class definition bound to no * statement — once the static side is a value, the expression IS the * definition plus a classRef over it. A DECORATED class expression @@ -6041,8 +6066,16 @@ export function lowerClassExpressionInfo( * mints a class at all: evaluating the expression IS the * ReferenceError, so it lowers to exactly that read — every evaluation * throws identically, which is why the once-evaluated restriction and - * the member fences don't apply. */ -export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr { + * the member fences don't apply. + * + * `binding` is the local of a function-local class declaration's name, + * declared without a value; the class value assigns it before the class's + * static members are defined (see initializeRuntimeStatics). */ +export function lowerClassExpression( + lowerer: Lowerer, + expr: ts.ClassLikeDeclaration, + binding?: IrLocal, +): IrExpr { if ( decoratorNodesOf(expr).length > 0 || expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0) @@ -6058,6 +6091,41 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara } } const info = lowerClassExpressionInfo(lowerer, expr); + // A function-local class expression's own name is a binding of its body: + // declared before the members lower, so their references to the name + // resolve to it. The class value captures it before it is assigned, as + // it captures a class declaration's binding. + if ( + !binding && + info.localClass && + ts.isClassExpression(expr) && + expr.name && + classExpressionNamesItself(lowerer, expr) + ) { + const own = lowerer.declareLocal( + expr.name, + expr.name.text, + { kind: "classval", className: info.def.name }, + true, + ); + const value = lowerClassValue(lowerer, expr, info, own); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: own.id, init: null, loc: value.loc }], + result: value, + type: value.type, + loc: value.loc, + }; + } + return lowerClassValue(lowerer, expr, info, binding); +} + +function lowerClassValue( + lowerer: Lowerer, + expr: ts.ClassLikeDeclaration, + info: ClassInfo, + binding: IrLocal | undefined, +): IrExpr { if ( !info.localClass && !info.expressionInitializationEmitted && @@ -6104,7 +6172,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc }; const boxed: IrExpr = { kind: "dynFrom", value: clsRef, type: DYN, loc }; info.localPrototypeData = true; - return initializeRuntimeStatics(lowerer, info, { + const built: IrExpr = { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: baseLocal.id, init: base, loc }, @@ -6137,7 +6205,8 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara result: clsRef, type: value.type, loc, - }); + }; + return initializeRuntimeStatics(lowerer, info, built, binding); } if (info.localClass?.baseExpression && info.base && value.kind === "classRef") { const loc = locOf(expr); @@ -6152,7 +6221,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc }; const basePrototype = classPrototypeData(lowerer, info.base, loc, baseRef)!; info.localPrototypeData = true; - return initializeRuntimeStatics(lowerer, info, { + const built: IrExpr = { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: baseLocal.id, init: base, loc }, @@ -6191,13 +6260,15 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara result: clsRef, type: value.type, loc, - }); + }; + return initializeRuntimeStatics(lowerer, info, built, binding); } if (!info.localClass) return value; const classValue = initializeRuntimeStatics( lowerer, info, value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value, + binding, ); // The base resolves statically here, but Node still evaluates the // heritage expression each time the class definition runs, first. @@ -7124,12 +7195,102 @@ export function inHierarchy(lowerer: Lowerer, info: ClassInfo): boolean { * dynamically. Abstract re-declarations don't count (they carry no * implementation; the concrete ones below them do, via the recursion). */ export function overrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean { + if (info.decl !== null && localOverrideBelow(lowerer, info.decl, name)) return true; return info.subclasses.some((s) => { const m = s.methods.get(name); return (m !== undefined && m.abstract !== true) || lowerer.overrideBelow(s, name); }); } +/** The class node an `extends` clause names: a class declaration, or the + * class expression a variable is initialized with. */ +function extendedClassNode(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): ts.Node | null { + const heritage = decl.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (!heritage || !ts.isIdentifier(heritage)) return null; + let symbol = lowerer.checker.getSymbolAtLocation(heritage); + if (symbol && symbol.flags & ts.SymbolFlags.Alias) + symbol = lowerer.checker.getAliasedSymbol(symbol); + const target = symbol?.valueDeclaration; + if (!target) return null; + if (ts.isClassDeclaration(target) || ts.isClassExpression(target)) return target; + if (ts.isVariableDeclaration(target) && target.initializer) { + let init: ts.Expression = target.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isClassExpression(init)) return init; + } + return null; +} + +/** The function-local classes that extend the class node `base`. Function + * bodies lower on demand, after code that may already call methods of a + * class their local classes extend, so such a subclass is not yet in its + * base's `subclasses` when those calls lower. */ +function localSubclassesOf(lowerer: Lowerer, base: ts.Node): readonly ts.ClassLikeDeclaration[] { + let index = lowerer.localSubclassIndex; + if (index === null) { + index = new Map(); + const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; + for (const sf of files) { + if (!sf.text.includes("extends")) continue; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + node.heritageClauses !== undefined && + functionLocalClass(node) + ) { + const extended = extendedClassNode(lowerer, node); + if (extended) { + const list = index!.get(extended); + if (list) list.push(node); + else index!.set(extended, [node]); + } + } + return undefined; + }); + } + lowerer.localSubclassIndex = index; + } + return index.get(base) ?? []; +} + +/** True when a function-local class below the class node `base` declares + * a concrete instance method or accessor `name` (accessors are keyed + * `get:x` and `set:x`). A call on `base`'s class then dispatches on the + * receiver; devirtualization turns it back into a direct call when no such + * override is reachable. */ +function localOverrideBelow( + lowerer: Lowerer, + base: ts.Node, + name: string, + visited?: Set, +): boolean { + const subclasses = localSubclassesOf(lowerer, base); + if (subclasses.length === 0) return false; + visited ??= new Set(); + if (visited.has(base)) return false; + visited.add(base); + const accessor = name.startsWith("get:") ? "get" : name.startsWith("set:") ? "set" : null; + const member = accessor ? name.slice(4) : name; + return subclasses.some( + (sub) => + sub.members.nodes.some( + (m) => + (accessor === null + ? ts.isMethodDeclaration(m) + : accessor === "get" + ? ts.isGetAccessorDeclaration(m) + : ts.isSetAccessorDeclaration(m)) && + m.body !== undefined && + m.name !== undefined && + (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) && + m.name.text === member && + !ts.getModifiers(m)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + ) || localOverrideBelow(lowerer, sub, name, visited), + ); +} + /** The nearest GENERIC-method declaration of `name` at/above `info` — * findMethodOn's twin over the genericMethods tables. */ export function findGenericMethodOn( diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..92c1946d5 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1523,6 +1523,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt const info = lowerer.lowerClassExpressionInfo(stmt); const type: IrType = { kind: "classval", className: info.def.name }; const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true); + // Static members of a function-local class run while the class value is + // built and may read the class by name: the class value assigns the + // binding before them. + if (info.localClass && (info.runtimeStatics || info.runtimePrototypeMembers)) + return [ + { kind: "varDecl", localId: local.id, init: null, loc: locOf(stmt) }, + { kind: "exprStmt", expr: lowerer.lowerClassExpression(stmt, local), loc: locOf(stmt) }, + ]; const init = lowerer.lowerClassExpression(stmt); return { kind: "varDecl", localId: local.id, init, loc: locOf(stmt) }; } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index c95543c61..7334455af 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3303,6 +3303,9 @@ export class Lowerer { * the class and constructed-class counts, it tells caches over the class * graph when the graph changed. */ subclassLinks = 0; + /** Function-local classes by the class node they extend, built on first + * use (localSubclassesOf in lower-classes.ts). */ + localSubclassIndex: Map | null = null; /** Set while a subclass constructor's super() call notes its base * constructor edge: that edge does not instantiate the base exactly. */ private superConstructorEdge = false; @@ -11377,8 +11380,8 @@ export class Lowerer { } } - lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr { - return lowerClassExpression(this, expr); + lowerClassExpression(expr: ts.ClassLikeDeclaration, binding?: IrLocal): IrExpr { + return lowerClassExpression(this, expr, binding); } /* ── the class graph (single inheritance) ─────────────────────────── */ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index e814bc0f5..923c857fe 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -3366,6 +3366,17 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): * tamper-guard prologue) registered in stdlibGlobalAliases; * - a const binding of the Map or Set constructor (`const M = Map`, * builtinClassAliasOf). */ +/** The symbol an identifier reads. A shorthand property's name (`{ Date }`) + * reads the binding of the same name, not the property it declares. */ +export function identifierReadSymbol(lowerer: Lowerer, expr: ts.Identifier): ts.Symbol | undefined { + if (expr.parent && ts.isShorthandPropertyAssignment(expr.parent) && expr.parent.name === expr) + return ( + lowerer.checker.getShorthandAssignmentValueSymbol(expr.parent) ?? + lowerer.checker.getSymbolAtLocation(expr) + ); + return lowerer.checker.getSymbolAtLocation(expr); +} + export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null { if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression); // The global-object detection chain (`typeof global < "u" ? global : @@ -3377,7 +3388,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return stdlibGlobalNameOf(lowerer, taken ? expr.whenTrue : expr.whenFalse); } if (ts.isIdentifier(expr)) { - const symbol = lowerer.checker.getSymbolAtLocation(expr); + const symbol = identifierReadSymbol(lowerer, expr); // `globalThis` itself: a reserved intrinsic — tsc rejects user // global bindings, but local parameters can shadow it. Its special // symbol has no ordinary declarations for the provenance check. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json new file mode 100644 index 000000000..bd7194022 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6913-builtin-constructor-shorthand.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6913-builtin-constructor-shorthand.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json new file mode 100644 index 000000000..9bc7d8342 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6914-dynamic-builtin-construct.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6914-dynamic-builtin-construct.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json new file mode 100644 index 000000000..a817ad3d2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-dynamic-builtin-construct.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-dynamic-builtin-construct.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json new file mode 100644 index 000000000..aaefe846a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-local-class-static-self-reference.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6916-local-class-static-self-reference.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json new file mode 100644 index 000000000..41d005bd0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6917-local-class-expression-self-reference.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6917-local-class-expression-self-reference.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json new file mode 100644 index 000000000..afed7a70d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6918-local-class-expression-self-reference.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6918-local-class-expression-self-reference.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json new file mode 100644 index 000000000..229c01bcd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6919-local-class-inspect-name.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6919-local-class-inspect-name.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json new file mode 100644 index 000000000..1878554c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6921-local-subclass-override-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6921-local-subclass-override-dispatch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json new file mode 100644 index 000000000..4da840cbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6922-local-subclass-override-dispatch.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index facf9f2ff..8a29e7c5d 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -659,6 +659,9 @@ ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed); ScrDyn *scr_dyn_native_set(ScrMap *value); /* borrowed backing, +1 box */ bool scr_dyn_native_set_is(const ScrDyn *value); ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */ +/* `new Map(entries)` over a checked-dynamic seed: +1, or NULL with the + * exception pending. */ +ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed); bool scr_dyn_native_map_is(const ScrDyn *value); void scr_regex_reset_last_index(ScrRegex *re, double index); ScrRegex *scr_regex_new_checked(const ScrDyn *pattern, const ScrDyn *flags); @@ -879,6 +882,12 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); bool scr_dyn_data_view_is(const ScrDyn *value); ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); +/* Installed by scr_dyn_invoke.c when it interns a builtin constructor value + * (scr_dyn_global_constructor): false when `callee` is not one of them; + * otherwise true with `new callee(...args)` in *result (+1, or NULL with + * the exception pending). */ +extern SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, + ScrDyn **result); ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key); /* The Buffer-flavored twin (stream chunks): string coercion/toString * decode utf8 instead of joining elements. */ diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 80e01b08c..e978ac7aa 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1854,6 +1854,8 @@ static const char dyn_s29[] SCR_DYN_COMPAT_STR = "Date"; static const char dyn_s30[] SCR_DYN_COMPAT_STR = "Map"; static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set"; static const char dyn_s32[] SCR_DYN_COMPAT_STR = ""; +static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet"; +static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive"; static const char *const dyn_global_ctor_names[] = { dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, @@ -1887,12 +1889,42 @@ static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, si return NULL; } +/* `new C(...args)` through a stored Object, Date, Map, Set, WeakMap or + * WeakSet builds what the `new` expression builds. RegExp, Promise, and + * Object over a primitive (a wrapper object) are fenced. */ +static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) { + if (callee->kind != SCR_DYN_FUNC || callee->v.fn.thunk != dyn_global_ctor_call) return false; + const char *name = callee->v.fn.clo->fn; + ScrDyn *const *items = args->v.arr.items; + size_t argc = args->v.arr.len; + const ScrDyn *first = argc ? items[0] : scr_dyn_undefined(); + bool primitive = first->kind == SCR_DYN_NUM || first->kind == SCR_DYN_STR || first->kind == SCR_DYN_BOOL || + first->kind == SCR_DYN_BIGINT || first->kind == SCR_DYN_SYMBOL; + if (strcmp(name, dyn_s26) == 0 && !primitive) *result = dyn_global_ctor_call(callee->v.fn.clo, items, argc); + else if (strcmp(name, dyn_s29) == 0) + *result = argc == 0 ? scr_dyn_native_date_new(args) + : argc == 1 ? scr_dyn_native_date_new_value(args) + : scr_dyn_native_date_new_fields(args); + else if (strcmp(name, dyn_s30) == 0) *result = scr_dyn_native_map_new(first); + else if (strcmp(name, dyn_s31) == 0) *result = scr_dyn_native_set_new(first); + else if (strcmp(name, dyn_s22) == 0) *result = scr_weak_map_new((ScrDyn *)first); + else if (strcmp(name, dyn_s24) == 0) *result = scr_weak_set_new((ScrDyn *)first); + else { + char message[128]; + int length = snprintf(message, sizeof message, dyn_s33, name, strcmp(name, dyn_s26) == 0 ? dyn_s34 : dyn_s32); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, dyn_s27); + *result = NULL; + } + return true; +} + static ScrDyn *dyn_global_ctor(const char *name) { for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { if (strcmp(dyn_global_ctor_names[i], name) != 0) continue; if (!dyn_global_ctors_registered) { dyn_global_ctors_registered = true; scr_atexit(dyn_global_ctors_cleanup); + scr_dyn_global_construct_fn = dyn_global_construct; } if (!dyn_global_ctors[i]) { /* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1, diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index f610ffdbb..6986330f5 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4017,6 +4017,8 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; } +SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = 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) return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len); @@ -4033,6 +4035,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS scr_bytes_release(bytes); return result; } + ScrDyn *constructed = NULL; + if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed)) + return constructed; ScrJsonBuf message; scr_jb_init(&message); scr_jb_write(&message, what->data, what->len); @@ -4180,6 +4185,13 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { if (!cls->prototype_data) { ScrDyn *data = base->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : scr_dyn_obj_create(base); if (!data) return NULL; + /* util.inspect prints an instance with its class's name. */ + const char *name = value->v.fn.name; + if (name && name[0]) { + ScrStr *class_name = scr_str_new(name, strlen(name)); + scr_dyn_name_class_prototype(data, class_name); + scr_str_release(class_name); + } cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } @@ -14939,6 +14951,27 @@ ScrDyn *scr_dyn_native_map(ScrMap *value) { return scr_dyn_new_handle(value, SCR_DYNH_MAP); } +ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed) { + ScrDyn *entries = scr_dyn_map_seed_entries(seed); + if (!entries) return NULL; + ScrMap *map = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_REF, &scr_map_dyn_map_ops); + for (size_t i = 0; i < entries->v.arr.len; i++) { + ScrDyn *entry = scr_dyn_map_seed_entry(entries->v.arr.items[i]); + if (!entry) break; + ScrDyn *key = scr_dyn_entry_read(entry, 0); + ScrDyn *value = scr_exc_pending() ? NULL : scr_dyn_entry_read(entry, 1); + scr_dyn_release(entry); + if (value) scr_map_dyn_set(map, key, value, false); + scr_dyn_release(key); + scr_dyn_release(value); + if (scr_exc_pending()) break; + } + scr_dyn_release(entries); + ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_native_map(map); + scr_map_release(map); + return result; +} + bool scr_dyn_native_map_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_MAP; } diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 99caa31a8..9e53b1e98 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1527,7 +1527,7 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops const ScrMapOps scr_map_dyn_set_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL, NULL, NULL, NULL }; -static const ScrMapOps scr_map_dyn_map_ops = { +const ScrMapOps scr_map_dyn_map_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL }; diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index c61df9a90..cce327508 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -214,8 +214,10 @@ void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length); * adapters); NULL keeps the source's descriptor. */ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops); ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only); -/* The descriptor of a Set of checked values (SCR_MAP_KEY_DYN keys). */ +/* The descriptors of a Set and a Map of checked values (SCR_MAP_KEY_DYN + * keys; the Map's values are checked values too). */ extern const ScrMapOps scr_map_dyn_set_ops; +extern const ScrMapOps scr_map_dyn_map_ops; void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked); void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops); diff --git a/tests/corpus/6913-builtin-constructor-shorthand.ts b/tests/corpus/6913-builtin-constructor-shorthand.ts new file mode 100644 index 000000000..7fb5066ed --- /dev/null +++ b/tests/corpus/6913-builtin-constructor-shorthand.ts @@ -0,0 +1,10 @@ +// Builtin constructors in shorthand properties are the constructors themselves. +const direct: Record = { Date: Date, Map: Map, Set: Set }; +const shorthand: Record = { Date, Map, Set, Object, Promise, WeakMap }; +console.log(shorthand.Date === Date, shorthand.Map === direct.Map, shorthand.Set === direct.Set); +console.log(shorthand.Object === Object, shorthand.Promise === Promise, shorthand.WeakMap === WeakMap); +console.log(typeof shorthand.Date, typeof shorthand.Map); +function pick(table: Record, name: string): unknown { + return table[name]; +} +console.log(pick({ Map, Set }, "Set") === Set, pick({ Date }, "Date") === pick(direct, "Date")); diff --git a/tests/corpus/6914-dynamic-builtin-construct.ts b/tests/corpus/6914-dynamic-builtin-construct.ts new file mode 100644 index 000000000..c026a6691 --- /dev/null +++ b/tests/corpus/6914-dynamic-builtin-construct.ts @@ -0,0 +1,54 @@ +// `new` through values that hold builtin constructors. +const ctors: Record = { Date, Map, Set, Object, WeakMap, WeakSet }; +function construct(name: string, ...args: unknown[]): unknown { + const C = ctors[name] as any; + return new C(...args); +} +function make(C: any, ...args: unknown[]): unknown { + return new C(...args); +} +const tag = (value: unknown) => Object.prototype.toString.call(value); + +const d = construct("Date", 0) as Date; +console.log(tag(d), d.getTime(), d.toISOString()); +const d2 = make(Date, 2020, 1, 3, 4, 5, 6, 7) as Date; +console.log(d2.getFullYear(), d2.getMonth(), d2.getDate(), d2.getHours(), d2.getMilliseconds()); +const d3 = make(Date, "2001-02-03T04:05:06Z") as Date; +const d4 = make(Date, d3) as Date; +console.log(d3.getTime(), d4.getTime()); +console.log(typeof make(Date), Number.isNaN((make(Date, "nope") as Date).getTime())); + +const m = construct("Map", [["a", 1], ["b", 2]]) as Map; +console.log(tag(m), m.size, m.get("b"), [...m.keys()].join()); +const m2 = make(Map, m) as Map; +m2.set("c", 3); +console.log(m.size, m2.size, (make(Map) as Map).size, (make(Map, null) as Map).size); + +const s = construct("Set", [1, 2, 2, 3]) as Set; +console.log(tag(s), s.size, s.has(3), (make(Set, "abca") as Set).size); + +const o = construct("Object") as Record; +o.x = 1; +const same = { y: 2 }; +console.log(tag(o), JSON.stringify(o), make(Object, same) === same, tag(make(Object, null))); + +const key = {}; +const w = construct("WeakMap", [[key, "v"]]) as WeakMap; +console.log(tag(w), w.get(key), w.has({})); +const ws = make(WeakSet, [key]) as WeakSet; +console.log(tag(ws), ws.has(key)); + +try { + make(Map, [1]); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +try { + make(WeakMap, [[1, 2]]); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +const DateSlot: new () => unknown = Date; +const MapSlot: new (entries: [string, number][]) => unknown = Map; +console.log(tag(new DateSlot()), (new MapSlot([["k", 1]]) as Map).get("k")); diff --git a/tests/corpus/6915-dynamic-builtin-construct.mjs b/tests/corpus/6915-dynamic-builtin-construct.mjs new file mode 100644 index 000000000..dd13aa325 --- /dev/null +++ b/tests/corpus/6915-dynamic-builtin-construct.mjs @@ -0,0 +1,16 @@ +// `new` through stored builtin constructors in JavaScript. +const table = [Date, Map, Set, Object, WeakMap, WeakSet]; +for (const C of table) { + const value = new C(); + console.log(C.name, typeof value, Object.prototype.toString.call(value), value.constructor === C); +} +const Ctor = table[0]; +console.log(new Ctor(86400000).toISOString(), new Ctor(2024, 0, 31).getDate()); +const MapCtor = table[1]; +const map = new MapCtor(Object.entries({ a: 1, b: 2 })); +map.set("c", 3); +console.log(map.size, map.get("a"), [...map.entries()].length); +const SetCtor = table[2]; +console.log(new SetCtor([...map.keys()]).size, new SetCtor([NaN, NaN, 0, -0]).size); +const named = { Date, Map, Set }; +console.log(named.Date === Date, named.Map === table[1], named.Set === table[2]); diff --git a/tests/corpus/6916-local-class-static-self-reference.mjs b/tests/corpus/6916-local-class-static-self-reference.mjs new file mode 100644 index 000000000..bc19cd0e9 --- /dev/null +++ b/tests/corpus/6916-local-class-static-self-reference.mjs @@ -0,0 +1,52 @@ +// Static fields, static blocks and static methods of function-local classes +// that read the class by its name while the class is being defined. +function registry(prefix) { + class Registry { + static label = Registry.name + ":" + prefix; + static instances = []; + static { + Registry.ready = Registry.instances.length === 0; + } + constructor(id) { + this.id = id; + Registry.instances.push(this); + } + static create(id) { + return new Registry(id); + } + } + return Registry; +} +const R = registry("main"); +R.create(1); +R.create(2); +console.log(R.label, R.ready, R.instances.map((r) => r.id)); +const R2 = registry("other"); +console.log(R2.label, R2.instances.length, R2 === R, R.instances.length); + +function counter(step) { + class Counter { + static zero = new Counter(0); + static one = Counter.zero.next(); + constructor(value) { + this.value = value; + } + next() { + return new Counter(this.value + step); + } + } + return Counter; +} +const C = counter(5); +console.log(C.zero.value, C.one.value, C.one.next().value, C.one instanceof C); + +function described() { + class Shape { + static kinds = ["circle", "square"]; + static describe() { + return Shape.kinds.map((kind) => kind + "@" + Shape.name).join(","); + } + } + return Shape.describe(); +} +console.log(described()); diff --git a/tests/corpus/6917-local-class-expression-self-reference.ts b/tests/corpus/6917-local-class-expression-self-reference.ts new file mode 100644 index 000000000..f11746eda --- /dev/null +++ b/tests/corpus/6917-local-class-expression-self-reference.ts @@ -0,0 +1,41 @@ +// Function-local class expressions that refer to themselves by their own name. +function counter(start: number) { + const C = class Counter { + value: number; + constructor(value: number) { this.value = value; } + next(): Counter { return new Counter(this.value + 1); } + static first(): Counter { return new Counter(start); } + static isSelf(value: unknown): boolean { return value === Counter; } + }; + return C; +} +const Counter1 = counter(10); +console.log(Counter1.first().next().next().value); +console.log(Counter1.isSelf(Counter1), Counter1.isSelf(counter(1)), Counter1.name); + +function box(seed: T) { + return class Box { + value: T; + constructor(value: T) { this.value = value; } + again(): Box { return new Box(this.value); } + static of(): Box { return new Box(seed); } + }; +} +const StringBox = box("seed"); +console.log(new StringBox("x").again().value, StringBox.of().value); +const NumberBox = box(7); +console.log(NumberBox.of().again().value + 1); + +function nested(n: number) { + const Outer = class Tree { + children: Tree[] = []; + static build(depth: number): Tree { + const tree = new Tree(); + if (depth > 0) tree.children.push(Tree.build(depth - 1), Tree.build(depth - 1)); + return tree; + } + size(): number { return 1 + this.children.reduce((total, child) => total + child.size(), 0); } + }; + return Outer.build(n).size(); +} +console.log(nested(3)); diff --git a/tests/corpus/6918-local-class-expression-self-reference.mjs b/tests/corpus/6918-local-class-expression-self-reference.mjs new file mode 100644 index 000000000..cfc8de806 --- /dev/null +++ b/tests/corpus/6918-local-class-expression-self-reference.mjs @@ -0,0 +1,50 @@ +// Function-local class expressions in JavaScript whose static members and +// methods read the class by its own name. +function point(scale) { + const P = class Point { + static origin = new Point(0, 0); + static unit = Point.scaled(1); + constructor(x, y) { + this.x = x; + this.y = y; + } + static scaled(n) { + return new Point(n * scale, n * scale); + } + plus(other) { + return new Point(this.x + other.x, this.y + other.y); + } + }; + return P; +} +const P = point(3); +const sum = P.origin.plus(P.unit).plus(P.unit); +console.log(sum.x, sum.y, P.unit instanceof P, P.name); + +function selfName() { + const K = class Named { + static who() { + return Named === this; + } + }; + return [K.who(), K.name, typeof K]; +} +console.log(selfName()); + +function factory(tag) { + return class Tagged { + static count = 0; + constructor() { + Tagged.count++; + } + static make() { + return new Tagged(); + } + get tag() { + return tag + Tagged.count; + } + }; +} +const T = factory("t"); +T.make(); +console.log(T.make().tag, T.count, factory("u").count); diff --git a/tests/corpus/6919-local-class-inspect-name.mjs b/tests/corpus/6919-local-class-inspect-name.mjs new file mode 100644 index 000000000..022895591 --- /dev/null +++ b/tests/corpus/6919-local-class-inspect-name.mjs @@ -0,0 +1,51 @@ +// console.log prints instances of function-local classes with prototype +// methods under their class names. +function point() { + class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + plus(other) { + return new Point(this.x + other.x, this.y + other.y); + } + } + return Point; +} +const Point = point(); +const a = new Point(1, 2); +console.log(a, a.plus(a)); +console.log([new Point(0, 0)], { p: new Point(5, 6) }); + +function animals() { + class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } + } + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + return [new Animal("cat"), new Dog("rex")]; +} +const [cat, dog] = animals(); +console.log(cat, dog); + +function anonymous() { + const Named = class { + value() { + return 1; + } + }; + return [new Named(), new (class { + value() { + return 2; + } + })()]; +} +console.log(anonymous()); diff --git a/tests/corpus/6921-local-subclass-override-dispatch.ts b/tests/corpus/6921-local-subclass-override-dispatch.ts new file mode 100644 index 000000000..0476e56c7 --- /dev/null +++ b/tests/corpus/6921-local-subclass-override-dispatch.ts @@ -0,0 +1,51 @@ +// Overrides declared by function-local subclasses dispatch on the receiver +// when the call site lowers before the function that declares them. +class Shape { + area(): number { return 0; } + get label(): string { return "shape"; } + describe(): string { return this.label + ":" + this.area(); } +} + +function square(side: number): Shape { + class Square extends Shape { + area(): number { return side * side; } + get label(): string { return "square"; } + } + return new Square(); +} + +function scaled(factor: number): Shape { + class Scaled extends Shape { + area(): number { return factor; } + } + class Doubled extends Scaled { + area(): number { return super.area() * 2; } + } + return new Doubled(); +} + +const shapes: Shape[] = [new Shape(), square(3), scaled(5)]; +for (const shape of shapes) console.log(shape.area(), shape.label, shape.describe()); + +function pair() { + class Base { + name(): string { return "base"; } + } + class Derived extends Base { + name(): string { return "derived"; } + } + return [new Base(), new Derived()]; +} +const [first, second] = pair(); +console.log(first.name(), second.name()); + +function counter() { + const Tally = class { + count(): number { return 1; } + }; + const Double = class extends Tally { + count(): number { return 2; } + }; + return new Double(); +} +console.log(counter().count()); diff --git a/tests/corpus/6922-local-subclass-override-dispatch.mjs b/tests/corpus/6922-local-subclass-override-dispatch.mjs new file mode 100644 index 000000000..8439c563d --- /dev/null +++ b/tests/corpus/6922-local-subclass-override-dispatch.mjs @@ -0,0 +1,51 @@ +// Overrides declared by function-local subclasses in JavaScript dispatch on +// the receiver when the call site lowers before the declaring function. +class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } +} + +function dog(name) { + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + return new Dog(name); +} + +function animals() { + class Bird extends Animal { + speak() { + return this.name + " sings"; + } + } + return [new Animal("cat"), new Bird("robin"), dog("rex")]; +} + +for (const animal of animals()) console.log(animal.speak()); +const speak = (animal) => animal.speak(); +console.log(speak(dog("fido"))); + +function pets() { + class Pet { + constructor(name) { + this.name = name; + } + greet() { + return this.name + " waves"; + } + } + class Cat extends Pet { + greet() { + return this.name + " purrs"; + } + } + return [new Pet("ann"), new Cat("tom")]; +} +const [pet, cat] = pets(); +console.log(pet.greet(), cat.greet()); diff --git a/tests/diagnostics/local-class-boundaries.ts b/tests/diagnostics/local-class-boundaries.ts index 2c43e9900..ac1afb2c2 100644 --- a/tests/diagnostics/local-class-boundaries.ts +++ b/tests/diagnostics/local-class-boundaries.ts @@ -1,10 +1,8 @@ function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } -function selfName() { return class Local { copy() { return new Local(); } }; } class Base {} function derived() { return class extends Base {}; } function generic() { return class { read(value: T): T { return value; } }; } privateBrand(); -selfName(); derived(); generic(); // End of unsupported local-class forms. diff --git a/tests/harness/__snapshots__/local-class-boundaries.ts.txt b/tests/harness/__snapshots__/local-class-boundaries.ts.txt index 6706a1278..697b1b8ed 100644 --- a/tests/harness/__snapshots__/local-class-boundaries.ts.txt +++ b/tests/harness/__snapshots__/local-class-boundaries.ts.txt @@ -2,18 +2,11 @@ local-class-boundaries.ts:1:34 - error SC1090: function-local classes with priva 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 2 | function selfName() { return class Local { copy() { return new Local(); } }; } + 2 | class Base {} -local-class-boundaries.ts:2:64 - error SC1090: named self references inside function-local classes are not supported yet +local-class-boundaries.ts:4:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - 1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; } - 2 | function selfName() { return class Local { copy() { return new Local(); } }; } - | ^~~~~ - 3 | class Base {} - -local-class-boundaries.ts:5:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - - 4 | function derived() { return class extends Base {}; } - 5 | function generic() { return class { read(value: T): T { return value; } }; } + 3 | function derived() { return class extends Base {}; } + 4 | function generic() { return class { read(value: T): T { return value; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | privateBrand(); \ No newline at end of file + 5 | privateBrand(); \ No newline at end of file diff --git a/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json b/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json new file mode 100644 index 000000000..3c9170116 --- /dev/null +++ b/tests/harness/corpus-costs/6913-builtin-constructor-shorthand.ts.json @@ -0,0 +1 @@ +{ "plain": 3202, "san": 4630 } diff --git a/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json b/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json new file mode 100644 index 000000000..a3f53b09c --- /dev/null +++ b/tests/harness/corpus-costs/6914-dynamic-builtin-construct.ts.json @@ -0,0 +1 @@ +{ "plain": 3064, "san": 5586 } diff --git a/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json b/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json new file mode 100644 index 000000000..0c65d046c --- /dev/null +++ b/tests/harness/corpus-costs/6915-dynamic-builtin-construct.mjs.json @@ -0,0 +1 @@ +{ "plain": 2821, "san": 4440 } diff --git a/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json b/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json new file mode 100644 index 000000000..0d9aa6b13 --- /dev/null +++ b/tests/harness/corpus-costs/6916-local-class-static-self-reference.mjs.json @@ -0,0 +1 @@ +{ "plain": 3135, "san": 4365 } diff --git a/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json b/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json new file mode 100644 index 000000000..2769f5b72 --- /dev/null +++ b/tests/harness/corpus-costs/6917-local-class-expression-self-reference.ts.json @@ -0,0 +1 @@ +{ "plain": 3074, "san": 4624 } diff --git a/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json b/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json new file mode 100644 index 000000000..04f903d8a --- /dev/null +++ b/tests/harness/corpus-costs/6918-local-class-expression-self-reference.mjs.json @@ -0,0 +1 @@ +{ "plain": 3050, "san": 4393 } diff --git a/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json b/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json new file mode 100644 index 000000000..78aea9163 --- /dev/null +++ b/tests/harness/corpus-costs/6919-local-class-inspect-name.mjs.json @@ -0,0 +1 @@ +{ "plain": 3058, "san": 4846 } diff --git a/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json b/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json new file mode 100644 index 000000000..0dd7e966b --- /dev/null +++ b/tests/harness/corpus-costs/6921-local-subclass-override-dispatch.ts.json @@ -0,0 +1 @@ +{ "plain": 2459, "san": 4630 } diff --git a/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json new file mode 100644 index 000000000..f2ba6c139 --- /dev/null +++ b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1 @@ +{ "plain": 3216, "san": 5042 } From e07f1be5c297a17769d7a2b69dcea17b6f78c2f5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:07:34 -0500 Subject: [PATCH 011/169] Keep aliases of evolving empty arrays on the array they alias An unannotated `const xs = []` inside a function takes the element type its reads agree on, and is checked-dynamic when they disagree. An alias taken between two writes of different element types (`const alias = xs` after `xs.push(1)`, before `xs.push("two")`) took the element type the checker gives it at that point, so it held a copy and missed the later writes: Node prints `1,two 2` for `xs.join(","), alias.length`, the compiled program printed `1,two 1`. An unannotated alias of such a binding, and an alias of that alias, now takes the binding's checked-dynamic type, so every name holds the one array. A write that stores a value the agreed element type does not hold (`xs.push("t")` after reads typed `boolean[]`, or `xs[1] = "one"`) now makes the binding checked-dynamic too. Those programs were refused with SC1090 "'string' values where 'boolean' is expected". Fixes #1641. --- .../src/frontend/lowering/lower-stmts.ts | 59 ++++++++++++++-- .../7006-evolving-array-aliases.ts.json | 6 ++ tests/corpus/7006-evolving-array-aliases.ts | 70 +++++++++++++++++++ .../7006-evolving-array-aliases.ts.json | 1 + 4 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json create mode 100644 tests/corpus/7006-evolving-array-aliases.ts create mode 100644 tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..29086d326 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -4799,9 +4799,10 @@ export function lowerVarDeclList( * `never[]` or `any[]` and each later read by the elements written before * it, and reads inside closures as the unsettled array. When every read * with a settled element type maps to the same array type, the binding has - * that type. When reads disagree or the binding is reassigned, it is - * checked-dynamic, as the same binding is at module scope. Null for other - * bindings. */ + * that type. When reads disagree, a write (`xs.push(v)`, `xs.unshift(v)`, + * `xs[i] = v`) stores a value that type's element does not hold, or the + * binding is reassigned, it is checked-dynamic, as the same binding is at + * module scope. Null for other bindings. */ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; let initializer: ts.Expression = decl.initializer; @@ -4823,6 +4824,9 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy }; const scope = owner(decl); let result: IrType | null = null; + // The values the binding's writes store, checked against the element + // type once the reads settle it. + const written: ts.Expression[] = []; // The file's identifiers are indexed by name once (identifiersNamed), so // each binding resolves only the uses spelled like it. for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) { @@ -4842,6 +4846,22 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment; if (reassigned) return DYN; + if ( + ts.isPropertyAccessExpression(parent) && + parent.expression === target && + (parent.name.text === "push" || parent.name.text === "unshift") && + ts.isCallExpression(parent.parent) && + parent.parent.expression === parent + ) + written.push(...parent.parent.arguments.nodes); + else if ( + ts.isElementAccessExpression(parent) && + parent.expression === target && + ts.isBinaryExpression(parent.parent) && + parent.parent.left === parent && + parent.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) + written.push(parent.parent.right); const t = lowerer.typeOf(node); if (unsettled(t)) continue; const mapped = lowerer.mapTypeOf(t); @@ -4849,7 +4869,36 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy return DYN; result = mapped; } - return result ?? DYN; + if (result === null || result.kind !== "array") return DYN; + for (const value of written) { + if (ts.isSpreadElement(value)) return DYN; + const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(value))); + if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null)) + return DYN; + } + return result; +} + +/** An unannotated alias of an evolving array binding whose reads disagree + * (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the + * checker types the alias by the elements written before it, but both names + * hold one array, so the alias takes the binding's checked-dynamic type and + * sees the later writes. Aliases of such an alias follow it. Null for other + * bindings. */ +function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { + let current = decl; + for (let depth = 0; depth < 8; depth++) { + let initializer = current.initializer; + while (initializer !== undefined && ts.isParenthesizedExpression(initializer)) + initializer = initializer.expression; + if (initializer === undefined || !ts.isIdentifier(initializer)) return null; + const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration; + if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null; + const evolved = evolvingArrayType(lowerer, source); + if (evolved !== null) return evolved.kind === "dyn" ? evolved : null; + current = source; + } + return null; } /** A single static type for an initializer-less JavaScript `let` whose @@ -5565,7 +5614,7 @@ export function lowerVarDecl( !decl.type && !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { - const evolved = evolvingArrayType(lowerer, decl); + const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl); if (evolved !== null) { type = evolved; init = lowerer.lowerExprExpecting(decl.initializer, evolved); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json new file mode 100644 index 000000000..cb3d36498 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7006-evolving-array-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7006-evolving-array-aliases.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7006-evolving-array-aliases.ts b/tests/corpus/7006-evolving-array-aliases.ts new file mode 100644 index 000000000..4810c440e --- /dev/null +++ b/tests/corpus/7006-evolving-array-aliases.ts @@ -0,0 +1,70 @@ +// An unannotated `const xs = []` inside a function whose writes store values +// of more than one element type: an alias taken between the writes +// (`const alias = xs`) and an alias of that alias name the same array, so +// they see every later write, as in Node. A write of another element type +// after reads that agree on one type also keeps the array untyped. +function chain(): string { + const xs = []; + xs.push(1); + const a = xs; + const b = a; + xs.push("two"); + b.push(3); + return `${xs.join(",")} ${a.length} ${b.length} ${a === xs} ${b === xs}`; +} +function viaAlias(): string { + const xs = []; + xs.push("a"); + const alias = xs; + alias.push("b"); + xs.push(2); + return `${xs.join(",")} ${alias.join(",")}`; +} +function letAlias(): string { + const xs = []; + xs.push(true); + let alias = xs; + xs.push("t"); + const seen = alias.length; + alias = []; + return `${seen} ${alias.length} ${xs.length}`; +} +function nested(flag: boolean): string { + const xs = []; + xs.push(1); + if (flag) { + const inner = xs; + xs.push("n"); + return `${inner.length} ${inner.join(":")}`; + } + return `${xs.length}`; +} +function elementWrite(): string { + const xs = []; + xs.push(1); + const alias = xs; + xs[1] = "one"; + return `${alias.length} ${alias.join("+")}`; +} +function agreeing(): string { + const xs = []; + xs.push(1); + const alias = xs; + xs.push(2); + return `${alias.length} ${alias === xs} ${alias.reduce((sum, v) => sum + v, 0)}`; +} +function total(values: number[]): number { + return values.length; +} +function passed(): string { + const xs = []; + xs.push(1); + const alias = xs; + const n = total(alias); + xs.push("z"); + return `${n} ${alias.length}`; +} +console.log(chain()); +console.log(viaAlias(), "|", letAlias()); +console.log(nested(true), "|", nested(false), "|", elementWrite()); +console.log(agreeing(), "|", passed()); diff --git a/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json new file mode 100644 index 000000000..f34efd332 --- /dev/null +++ b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json @@ -0,0 +1 @@ +{ "plain": 2292, "san": 3445 } From dd3ea7c00c80f3fa7eb3ba30d34863a28f2ed928 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:47:03 -0500 Subject: [PATCH 012/169] Narrow filter results through written type predicates with checked conversions `.filter` with a written type predicate (`(p): p is Cat => ...`, or a guard function passed as a value such as `filter(isDog)`) was refused with SC1090, because TypeScript does not check the predicate. An inferred predicate that keeps several union arms (`v => v !== undefined` over `(string | number | undefined)[]`) was refused too. The filter loop now keeps the receiver's element type, and the result converts to the predicate's type element by element (filter-predicates.ts): - a union element to one of its arms, or to a union of some of its arms, by its tag; - a checked-dynamic element by dynCheck; - a class instance to a subclass by its class; - a tuple or record to one with the same fields, field by field. An element the predicate's type does not describe, kept because the predicate returned true for it, throws a catchable TypeError: "a kept element is not a 'Cat' (a type predicate returned true for a value of another type)". The diagnostics fixture filter-narrow.ts, which pinned the three refusals, compiles now; corpus 7007 covers its cases. Fixes #1680. --- .../lowering/containers/array-callbacks.ts | 119 +++++++-- .../lowering/containers/filter-predicates.ts | 231 ++++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 33 ++- ...007-filter-written-type-predicates.ts.json | 6 + .../tests/diagnostics/filter-narrow.ts.json | 6 - .../7007-filter-written-type-predicates.ts | 87 +++++++ tests/diagnostics/filter-narrow.ts | 22 -- .../__snapshots__/filter-narrow.ts.txt | 20 -- ...007-filter-written-type-predicates.ts.json | 1 + 9 files changed, 435 insertions(+), 90 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/containers/filter-predicates.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json create mode 100644 tests/corpus/7007-filter-written-type-predicates.ts delete mode 100644 tests/diagnostics/filter-narrow.ts delete mode 100644 tests/harness/__snapshots__/filter-narrow.ts.txt create mode 100644 tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 612c47ea0..dcadf3bf3 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -59,6 +59,7 @@ import { } from "./array-iteration.js"; import { dynamicReceiverThrows } from "./dynamic-receivers.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js"; +import { checkedFilterNarrowing } from "./filter-predicates.js"; /** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct * call of a synthetic module function — one per method + element/result @@ -123,10 +124,11 @@ export function lowerArrayHofCall( } lowerer.badType(argNode, lowerer.typeOf(argNode)); } - const outElem = + const filtered = method === "filter" ? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter) - : callbackArrayElement(lowerer, call, fnRet); + : null; + const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet); // A filter that re-tags its elements keeps the single union-typed visit. const fast = present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem)) @@ -145,19 +147,33 @@ export function lowerArrayHofCall( present, ); const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID; - return { + const result: IrExpr = { kind: "call", callee: helper, args: [receiver, fnArg, ...(fast ? [fast] : [])], type: resultType, loc, }; + const narrowing = filtered?.narrowing ?? null; + if (narrowing === null) return result; + return { + kind: "call", + callee: narrowing.helper, + args: [result], + type: arrayOf(narrowing.elem), + loc, + }; } -/** The checker-selected element of a filter result, with the same trust - * boundary as the historical narrowing filter: an inferred inline type - * predicate may prove one source arm; a written predicate annotation may - * not. Boolean is safe when every discarded arm is a nullish unit. */ +/** The element a filter keeps, and the checked conversion of the result + * to the checker's narrower element when the predicate does not prove it. + * An inferred inline type predicate proves one source arm, or a class + * view, so the loop keeps that type directly. A written predicate (`(p): p + * is Cat => ...`, a guard function passed as a value) is an assertion + * TypeScript does not check, and an inferred predicate that keeps several + * arms has no single arm to re-tag: the loop keeps the receiver's element + * and `narrowing` (checkedFilterNarrowing) converts each kept element. + * Boolean is safe when every discarded arm is a nullish unit. */ function filterResultElem( lowerer: Lowerer, call: ts.CallExpression, @@ -165,7 +181,37 @@ function filterResultElem( elem: IrType, valueT: IrType, booleanFilter: boolean, -): IrType { +): { elem: IrType; narrowing: { helper: string; elem: IrType } | null } { + const kept = filterKeptElem(lowerer, call, argNode, elem, valueT, booleanFilter); + if (typeof kept !== "string") return { elem: kept, narrowing: null }; + return { elem, narrowing: { helper: kept, elem: filterCheckerElem(lowerer, call, elem) } }; +} + +/** The checker's spelling of a filter result's element type. */ +function filterTypeText(lowerer: Lowerer, call: ts.CallExpression): string { + const result = lowerer.typeOf(call); + const element = lowerer.checker.isArrayType(result) + ? lowerer.checker.getTypeArguments(result as ts.TypeReference)[0] + : undefined; + return element === undefined ? "" : lowerer.checker.typeToString(element); +} + +/** The checker's element of a filter result, or the receiver's when the + * filter introduces no refinement. */ +function filterCheckerElem(lowerer: Lowerer, call: ts.CallExpression, elem: IrType): IrType { + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + return result?.kind === "array" ? result.elem : elem; +} + +/** filterResultElem's element, or the name of the checked narrowing helper. */ +function filterKeptElem( + lowerer: Lowerer, + call: ts.CallExpression, + argNode: ts.Expression, + elem: IrType, + valueT: IrType, + booleanFilter: boolean, +): IrType | string { const callT = lowerer.typeOf(call); const result = lowerer.mapTypeOf(callT); let outElem = result?.kind === "array" ? result.elem : elem; @@ -191,23 +237,31 @@ function filterResultElem( "keep the receiver's element type instead — annotate the callback's return ': boolean' " + "(the checker then skips the predicate) or annotate the result with the receiver's own " + "element type — and narrow the elements after"; - if (!booleanFilter) { + const written = + !booleanFilter && + ((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || + argNode.type !== undefined); + const narrowing = booleanFilter + ? null + : (): string | null => + checkedFilterNarrowing(lowerer, elem, outElem, filterTypeText(lowerer, call), locOf(call)); + if (written) { + const helper = narrowing!(); + if (helper !== null) return helper; if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) { lowerer.unsupported( "SC1090", argNode, - `narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` + - `the checker inferred can re-tag — ${annotateEscape})`, - ); - } - if (argNode.type) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` + - `unchecked assertion nothing validates at runtime — ${annotateEscape})`, + `narrowing '.filter' to '${lowerer.fmt(outElem)}' through a callback VALUE (no checked ` + + `conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`, ); } + lowerer.unsupported( + "SC1090", + argNode, + `narrowing '.filter' to '${lowerer.fmt(outElem)}' with a hand-written type predicate (no ` + + `checked conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`, + ); } const classRefinement = !booleanFilter && outElem.kind === "object" && canAssertClassValue(lowerer, valueT, outElem); @@ -215,6 +269,8 @@ function filterResultElem( !classRefinement && (valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0) ) { + const helper = narrowing?.() ?? null; + if (helper !== null) return helper; const multiArm = outElem.kind === "union"; lowerer.unsupported( "SC1090", @@ -2650,6 +2706,7 @@ export function lowerTuplePredicateCall( // the value type an array find narrows from. let foundT: IrType = elem; let retag: string | null = null; + let narrowing: { helper: string; elem: IrType } | null = null; if (method === "filter") { // The element union is built in position order, so the checker's // result element can list the same arms in another order. @@ -2660,9 +2717,11 @@ export function lowerTuplePredicateCall( checkerResult?.kind === "array" && arms(checkerResult.elem).length === arms(elem).length && arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other))); - outElem = unchanged - ? elem - : filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter); + if (!unchanged) { + const filtered = filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter); + outElem = filtered.elem; + narrowing = filtered.narrowing; + } resultT = arrayOf(outElem); } else if (method === "find") { resultT = lowerer.irTypeOf(call); @@ -2795,5 +2854,19 @@ export function lowerTuplePredicateCall( loc, }); } - return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc }; + const result: IrExpr = { + kind: "call", + callee: helper, + args: [receiver, fnArg], + type: resultT, + loc, + }; + if (narrowing === null) return result; + return { + kind: "call", + callee: narrowing.helper, + args: [result], + type: arrayOf(narrowing.elem), + loc, + }; } diff --git a/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts new file mode 100644 index 000000000..df57813fe --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts @@ -0,0 +1,231 @@ +/* Checked narrowing for `.filter` with a written type predicate. + * + * A written predicate (`(p): p is Cat => ...`, or a guard function passed + * as a value) is an assertion TypeScript does not check, so the filter loop + * keeps the receiver's element type and the result converts to the + * predicate's type afterwards, element by element: + * + * - a union element to one of its arms, or to a union of some of its + * arms, by its tag; + * - a checked-dynamic element by dynCheck; + * - a class instance to a subclass by its class; + * - a tuple or record to one with the same fields, field by field. + * + * Each conversion throws the catchable TypeError of a checked extraction + * when a kept element is not of the predicate's type, which is when the + * predicate returned true for a value it does not describe. */ +import { varRef } from "../../../ir/build.js"; +import { + BOOL, + F64, + STRING, + isUnitType, + typeEquals, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; +import { checkedClassAssertion } from "../class-assertions.js"; +import { buildArrayConversion } from "../coercions/builders.js"; +import type { Lowerer } from "../lowerer.js"; +import { keyedHelperName } from "../stable-names.js"; +import { typeKey } from "../../type-mapper.js"; + +/** The interned helper converting a `from[]` filter result to `to[]`, or + * null when an element of `from` has no checked conversion to `to`. + * `typeText` names the predicate's type in the TypeError. */ +export function checkedFilterNarrowing( + lowerer: Lowerer, + from: IrType, + to: IrType, + typeText: string, + loc: SrcLoc, +): string | null { + const element = elementHelper(lowerer, from, to, loc, typeText); + if (element === null) return null; + const key = `filter.narrow:${typeKey(from)}:${typeKey(to)}:${typeText}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%arr.narrow", key); + lowerer.valueHelpers.set(key, name); + const outT: IrType = { kind: "array", elem: to }; + lowerer.liftedFns.push( + buildArrayConversion( + name, + { kind: "array", elem: from }, + { kind: "arrayLit", elems: [], type: outT, loc }, + (value, _index, result) => ({ + kind: "arrIntrinsic", + method: "push", + receiver: result, + args: [{ kind: "call", callee: element, args: [value], type: to, loc }], + type: F64, + loc, + }), + loc, + ), + ); + return name; +} + +/** The interned helper converting one `from` value to `to`, or null. */ +function elementHelper( + lowerer: Lowerer, + from: IrType, + to: IrType, + loc: SrcLoc, + typeText = lowerer.fmt(to), +): string | null { + const key = `filter.narrow.value:${typeKey(from)}:${typeKey(to)}:${typeText}`; + const existing = lowerer.valueHelpers.get(key); + if (existing) return existing; + // A recursive type pair converts through the helper being built. + if (lowerer.coercions.planning.has(key)) return null; + lowerer.coercions.planning.add(key); + const input = varRef("v.0", from, loc); + let body: IrStmt[] | null; + try { + body = conversionBody(lowerer, input, from, to, typeText, loc); + } finally { + lowerer.coercions.planning.delete(key); + } + if (body === null) return null; + const name = keyedHelperName(lowerer, "%narrow.checked", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: from }], + returnType: to, + locals: [{ id: "v.0", name: "v", type: from, mutable: false }], + body, + loc, + }); + return name; +} + +/** A converted value: `value` itself when the types are equal, or a call of + * the conversion helper. */ +function converted( + lowerer: Lowerer, + value: IrExpr, + from: IrType, + to: IrType, + loc: SrcLoc, +): IrExpr | null { + if (typeEquals(from, to)) return value; + const helper = elementHelper(lowerer, from, to, loc); + return helper === null ? null : { kind: "call", callee: helper, args: [value], type: to, loc }; +} + +/** The TypeError a kept element of another type throws. */ +function typeError(message: string, loc: SrcLoc): IrStmt { + return { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "strLit", value: message, type: STRING, loc }], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }; +} + +function conversionBody( + lowerer: Lowerer, + input: IrExpr, + from: IrType, + to: IrType, + typeText: string, + loc: SrcLoc, +): IrStmt[] | null { + if (typeEquals(from, to)) return [{ kind: "return", value: input, loc }]; + if (from.kind === "dyn") + return [{ kind: "return", value: { kind: "dynCheck", value: input, type: to, loc }, loc }]; + if (from.kind === "object" && to.kind === "object") { + const asserted = checkedClassAssertion(lowerer, input, to, loc); + return asserted === null ? null : [{ kind: "return", value: asserted, loc }]; + } + if (from.kind === "union") + return unionConversion(lowerer, input, from.unionId, to, typeText, loc); + if (from.kind === "record" && to.kind === "record") { + const source = lowerer.shapes.get(from.shapeId); + const target = lowerer.shapes.get(to.shapeId); + if ( + !source || + !target || + source.tuple !== target.tuple || + source.indexValue || + target.indexValue + ) + return null; + if (source.fields.length !== target.fields.length) return null; + const fields: { name: string; value: IrExpr }[] = []; + for (const field of target.fields) { + const own = source.fields.find((f) => f.name === field.name); + if (!own) return null; + const read: IrExpr = { + kind: "recordGet", + obj: input, + shapeId: from.shapeId, + field: field.name, + type: own.type, + loc, + }; + const value = converted(lowerer, read, own.type, field.type, loc); + if (value === null) return null; + fields.push({ name: field.name, value }); + } + return [{ kind: "return", value: { kind: "recordLit", fields, type: to, loc }, loc }]; + } + return null; +} + +/** A union value to one of its arms, or to a union of some of its arms: + * each arm the target holds returns, any other arm throws. */ +function unionConversion( + lowerer: Lowerer, + input: IrExpr, + unionId: string, + to: IrType, + typeText: string, + loc: SrcLoc, +): IrStmt[] | null { + const source = lowerer.unions.get(unionId); + if (!source) return null; + const targetArms = to.kind === "union" ? (lowerer.unions.get(to.unionId)?.arms ?? null) : [to]; + if (targetArms === null) return null; + const body: IrStmt[] = []; + let kept = 0; + for (let tag = 0; tag < source.arms.length; tag++) { + const arm = source.arms[tag]!; + const targetTag = targetArms.findIndex((t) => typeEquals(t, arm)); + if (targetTag < 0) continue; + kept++; + const payload: IrExpr = isUnitType(arm) + ? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc } + : { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc }; + const value: IrExpr = + to.kind === "union" + ? { kind: "unionWrap", unionId: to.unionId, tag: targetTag, value: payload, type: to, loc } + : payload; + body.push({ + kind: "if", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc }, + then: [{ kind: "return", value, loc }], + else_: null, + loc, + }); + } + // Every arm the target names must be one of the union's own arms. + if (kept === 0 || kept !== targetArms.length) return null; + body.push( + typeError( + `a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`, + loc, + ), + ); + return body; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..3acfb256a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -6668,6 +6668,15 @@ export function lowerFilterNarrowCall( } const narrowed = outElem !== null && !typeEquals(outElem, elem); if (!narrowed && !isBooleanArg) return null; + // A written predicate (`(p): p is Cat => ...`, a guard function value) + // proves nothing at run time: the generic array HOF path filters the + // receiver's elements and converts the kept ones checked + // (filterResultElem). + if ( + !isBooleanArg && + ((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || argNode.type) + ) + return null; const annotateEscape = "keep the receiver's element type instead — annotate the callback's return ': boolean' " + @@ -6679,6 +6688,9 @@ export function lowerFilterNarrowCall( lowerer.badType(call, callT); // defensive: a narrowed non-union receiver } tag = lowerer.armTag(elem.unionId, outElem); + // Several kept arms: the generic path converts the kept elements to + // the narrower union (filterResultElem). + if (tag < 0 && !isBooleanArg) return null; if (tag < 0) { lowerer.unsupported( "SC1090", @@ -6700,25 +6712,8 @@ export function lowerFilterNarrowCall( return { kind: "call", callee: helper, args: [receiver], type: arrayOf(outElem ?? elem), loc }; } - // Inferred type predicate: inline function literal, NO return - // annotation (a written one is an unchecked assertion), and the - // checker reports a predicate over parameter 0. - if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' through a callback VALUE ` + - `(only an inline callback whose predicate the checker inferred can re-tag — ${annotateEscape})`, - ); - } - if (argNode.type) { - lowerer.unsupported( - "SC1090", - argNode, - `narrowing '.filter' with a hand-written type predicate ` + - `(a written 'x is T' is an unchecked assertion nothing validates at runtime — ${annotateEscape})`, - ); - } + // An inferred type predicate: an inline function literal with no return + // annotation, over parameter 0 (written predicates returned above). // The receiver evaluates FIRST, in the enclosing function, like JS. const receiver = lowerer.lowerExpr(access.expression); const fnArg = lowerer.lowerExpr(argNode); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json new file mode 100644 index 000000000..d77ee1a57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7007-filter-written-type-predicates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7007-filter-written-type-predicates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json deleted file mode 100644 index 40d5e462a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/filter-narrow.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/filter-narrow.ts" - ], - "diags": [] -} diff --git a/tests/corpus/7007-filter-written-type-predicates.ts b/tests/corpus/7007-filter-written-type-predicates.ts new file mode 100644 index 000000000..c2449b469 --- /dev/null +++ b/tests/corpus/7007-filter-written-type-predicates.ts @@ -0,0 +1,87 @@ +// `.filter` with a written type predicate: an annotated callback +// (`(p): p is Cat => ...`) or a guard function passed as a value. The +// result holds the predicate's type: a union arm, a union of arms, a +// number out of `unknown`, a record or tuple with a narrower field, or a +// subclass. An inferred predicate that keeps several arms narrows too. +interface Cat { + kind: "cat"; + meow: string; +} +interface Dog { + kind: "dog"; + bark: number; +} +interface Fish { + kind: "fish"; + fins: number; +} +type Pet = Cat | Dog | Fish; +const pets: Pet[] = [ + { kind: "cat", meow: "m" }, + { kind: "dog", bark: 2 }, + { kind: "fish", fins: 3 }, + { kind: "dog", bark: 5 }, +]; + +const cats = pets.filter((p): p is Cat => p.kind === "cat"); +console.log(cats.length, cats[0]!.meow); +const furry = pets.filter((p): p is Cat | Dog => p.kind !== "fish"); +console.log(furry.map((p) => (p.kind === "cat" ? p.meow : p.bark)).join(",")); +const notFish = pets.filter((p) => p.kind !== "fish"); +console.log(notFish.length); + +function isDog(p: Pet): p is Dog { + return p.kind === "dog"; +} +console.log(pets.filter(isDog).map((d) => d.bark * 10).join(",")); + +const isNumber = (v: unknown): v is number => typeof v === "number"; +const values: unknown[] = JSON.parse('[1, "a", 2, null, 4]'); +console.log(values.filter(isNumber).reduce((sum, n) => sum + n, 0)); + +const rows: unknown[] = JSON.parse('[{"id":1},{"id":"x"},{"id":3}]'); +const withIds = rows.filter( + (r): r is { id: number } => typeof (r as { id: unknown }).id === "number", +); +console.log(withIds.map((r) => r.id + 1).join(",")); + +const entries: [string, unknown][] = [ + ["a", 1], + ["b", "x"], + ["c", 3], +]; +const numeric = entries.filter((e): e is [string, number] => typeof e[1] === "number"); +console.log(numeric.map(([key, value]) => `${key}${value * 2}`).join(" ")); + +class Shape { + name: string; + constructor(name: string) { + this.name = name; + } +} +class Circle extends Shape { + r: number; + constructor(r: number) { + super("circle"); + this.r = r; + } +} +const shapes: Shape[] = [new Shape("square"), new Circle(2), new Circle(3)]; +const circles = shapes.filter((s): s is Circle => s instanceof Circle); +console.log(circles.length, circles.map((c) => c.r).join("+")); + +const mixed: (string | number | boolean)[] = ["a", 1, true, "b"]; +console.log(mixed.filter((v): v is string => typeof v === "string").join("")); +console.log(mixed.filter((v): v is string | number => typeof v !== "boolean").length); + +const pair: [string | number, string | number] = ["x", 4]; +console.log(pair.filter((v): v is number => typeof v === "number").map((n) => n + 1).join()); + +const optional: (string | number | undefined)[] = ["a", 1, undefined, 2]; +const defined = optional.filter((v) => v !== undefined); +console.log(defined.length, defined.map((v) => typeof v).join(",")); +function isText(v: string | undefined): v is string { + return typeof v === "string"; +} +const maybe: (string | undefined)[] = ["a", undefined, "b"]; +console.log(maybe.filter(isText).join("-")); diff --git a/tests/diagnostics/filter-narrow.ts b/tests/diagnostics/filter-narrow.ts deleted file mode 100644 index 45452e212..000000000 --- a/tests/diagnostics/filter-narrow.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Narrowing filters re-tag ONLY what the runtime test proved: an inferred -// single-arm predicate or filter(Boolean). Everything else stays fenced — -// multi-arm targets (the re-tag doesn't exist), hand-written predicates -// (unchecked assertions), and predicate VALUES (their annotation is -// written by definition). -const xs: (string | number | undefined)[] = ["a", 1, undefined]; - -// Multi-arm narrowing: string | number is not a single arm. -const defined = xs.filter((x) => x !== undefined); -console.log(defined.length); - -// A hand-written predicate on an inline callback. -const ys: (string | undefined)[] = ["a", undefined]; -const strings = ys.filter((x): x is string => typeof x === "string"); -console.log(strings.length); - -// A predicate VALUE (declared function with a written predicate). -function isString(x: string | undefined): x is string { - return typeof x === "string"; -} -const viaValue = ys.filter(isString); -console.log(viaValue.length); diff --git a/tests/harness/__snapshots__/filter-narrow.ts.txt b/tests/harness/__snapshots__/filter-narrow.ts.txt deleted file mode 100644 index bc5180219..000000000 --- a/tests/harness/__snapshots__/filter-narrow.ts.txt +++ /dev/null @@ -1,20 +0,0 @@ -filter-narrow.ts:9:17 - error SC1090: '.filter' narrowing 'number | string | undefined' elements to the multi-arm 'number | string' (only a SINGLE arm re-tags — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 8 | // Multi-arm narrowing: string | number is not a single arm. - 9 | const defined = xs.filter((x) => x !== undefined); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(defined.length); - -filter-narrow.ts:14:27 - error SC1090: narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an unchecked assertion nothing validates at runtime — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 13 | const ys: (string | undefined)[] = ["a", undefined]; - 14 | const strings = ys.filter((x): x is string => typeof x === "string"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 15 | console.log(strings.length); - -filter-narrow.ts:21:28 - error SC1090: narrowing '.filter' through a callback VALUE (only an inline callback whose predicate the checker inferred can re-tag — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet - - 20 | } - 21 | const viaValue = ys.filter(isString); - | ^~~~~~~~ - 22 | console.log(viaValue.length); \ No newline at end of file diff --git a/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json b/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json new file mode 100644 index 000000000..188fa7fab --- /dev/null +++ b/tests/harness/corpus-costs/7007-filter-written-type-predicates.ts.json @@ -0,0 +1 @@ +{ "plain": 3510, "san": 2873 } From 423a70a1828fcda1590df5e264251c2c5c0f537f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 05:51:16 -0700 Subject: [PATCH 013/169] Compile zod 3 natively - An untyped JavaScript receiver whose declaring file assigns it an own toString (the namespace downlevel's errorUtil.toString) calls that function instead of the string-typed toString dispatch: zod's number checks threw a failed string check on its undefined result. - Number.parseInt with one JavaScript argument that only the global parser's forms lower compiles through those forms. - A JavaScript then whose declared result has no mapping settles with the handler's value as a checked-dynamic value. New npm cases: zod3 (real zod 3.25.67, v3 ESM build and types), and js-own-tostring. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/numbers.ts | 9 +- .../src/frontend/lowering/lower-calls.ts | 54 +- .../npm/cases/js-own-tostring/main.ts.json | 6 + .../fixtures/npm/cases/zod3/main.ts.json | 6 + .../npm/cases/js-own-tostring/main.ts | 14 + .../node_modules/nsutil/dist/errorUtil.d.ts | 9 + .../node_modules/nsutil/dist/errorUtil.js | 5 + .../node_modules/nsutil/dist/index.d.ts | 6 + .../node_modules/nsutil/dist/index.js | 14 + .../node_modules/nsutil/package.json | 1 + .../npm/cases/js-own-tostring/package.json | 1 + tests/fixtures/npm/cases/zod3/main.ts | 115 + .../npm/cases/zod3/node_modules/zod/LICENSE | 21 + .../zod3/node_modules/zod/dist/esm/index.js | 3 + .../node_modules/zod/dist/esm/package.json | 3 + .../node_modules/zod/dist/esm/v3/ZodError.js | 132 + .../node_modules/zod/dist/esm/v3/errors.js | 9 + .../node_modules/zod/dist/esm/v3/external.js | 6 + .../zod/dist/esm/v3/helpers/enumUtil.js | 1 + .../zod/dist/esm/v3/helpers/errorUtil.js | 6 + .../zod/dist/esm/v3/helpers/parseUtil.js | 109 + .../zod/dist/esm/v3/helpers/partialUtil.js | 1 + .../zod/dist/esm/v3/helpers/typeAliases.js | 1 + .../zod/dist/esm/v3/helpers/util.js | 133 + .../node_modules/zod/dist/esm/v3/index.js | 4 + .../zod/dist/esm/v3/locales/en.js | 107 + .../zod/dist/esm/v3/standard-schema.js | 1 + .../node_modules/zod/dist/esm/v3/types.js | 3691 +++++++++++++++++ .../node_modules/zod/dist/types/index.d.ts | 3 + .../zod/dist/types/v3/ZodError.d.ts | 164 + .../zod/dist/types/v3/errors.d.ts | 5 + .../zod/dist/types/v3/external.d.ts | 6 + .../zod/dist/types/v3/helpers/enumUtil.d.ts | 8 + .../zod/dist/types/v3/helpers/errorUtil.d.ts | 9 + .../zod/dist/types/v3/helpers/parseUtil.d.ts | 78 + .../dist/types/v3/helpers/partialUtil.d.ts | 8 + .../dist/types/v3/helpers/typeAliases.d.ts | 2 + .../zod/dist/types/v3/helpers/util.d.ts | 85 + .../node_modules/zod/dist/types/v3/index.d.ts | 4 + .../zod/dist/types/v3/locales/en.d.ts | 3 + .../zod/dist/types/v3/standard-schema.d.ts | 102 + .../node_modules/zod/dist/types/v3/types.d.ts | 1031 +++++ .../cases/zod3/node_modules/zod/package.json | 120 + tests/fixtures/npm/cases/zod3/package.json | 1 + 44 files changed, 6093 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json create mode 100644 tests/fixtures/npm/cases/js-own-tostring/main.ts create mode 100644 tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js create mode 100644 tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts create mode 100644 tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js create mode 100644 tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json create mode 100644 tests/fixtures/npm/cases/js-own-tostring/package.json create mode 100644 tests/fixtures/npm/cases/zod3/main.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/index.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts create mode 100644 tests/fixtures/npm/cases/zod3/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/cases/zod3/package.json diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 1ff35fbb5..9764216b7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -1,7 +1,7 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { lowerCheckedPredicateValue } from "../lower-builtin-values.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, F64, STRING, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js"; import { tryLowerExpression } from "../expressions/try-lower-expression.js"; @@ -104,6 +104,13 @@ export function lowerNumberStaticCall( : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } + // JavaScript whose argument does not lower on its own (zod's + // `Number.parseInt(val.toFixed(n).replace(".", ""))` over an untyped + // parameter): the global parser's own JavaScript forms. + if (probed === null && member === "parseInt" && isJsSourceFile(ts.getSourceFileOfNode(call))) { + const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); + if (parsed !== null) return parsed; + } const argText = probed !== null ? lowerer.fmt(probed.type) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..7e104c2f1 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3776,7 +3776,14 @@ function lowerDynReceiverMethodCall( } // The argument is a radix for bigint and an encoding for Buffer. // Preserve its runtime value so receiver dispatch can choose correctly. - if (access.name.text === "toString" && call.arguments.nodes.length <= 1) { + // An authored own `toString` member (zod's `errorUtil.toString`, which + // answers string | undefined) is an ordinary method call instead. + if ( + access.name.text === "toString" && + call.arguments.nodes.length <= 1 && + !ownCallable && + !(checkerUntyped && jsAssignsOwnToString(lowerer, access.expression)) + ) { const argument = call.arguments.nodes[0]; return { kind: "libCall", @@ -3886,12 +3893,22 @@ function lowerDynReceiverMethodCall( // The same holds when only the MEMBER is checker-untyped: TypeScript // importing chalk's JavaScript source types the default export as a // plain function, so `chalk.bold` reads as `any` on a typed receiver. + const ownToString = + access.name.text === "toString" && + checkerUntyped && + jsAssignsOwnToString(lowerer, access.expression); const stringOnlyName = (checkerUntyped || (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text); - if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null; + if ( + DYN_PROTO_METHOD_NAMES.has(access.name.text) && + !ownCallable && + !ownToString && + !stringOnlyName + ) + return null; // Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain // machinery's short-circuit semantics — not modeled here yet. if (call.questionDotToken || access.questionDotToken) return null; @@ -6175,7 +6192,21 @@ export function lowerPromiseMethodCall( `then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`, ); } - const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + let resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + // JavaScript whose declared result type has no mapping (zod's + // `Promise.all(items).then((r) => ParseStatus.mergeArray(status, r))`, + // typed by a .d.ts union): the handler's value settles the promise as + // a checked-dynamic value, checked where it is used. + if ( + resultT?.kind !== "promise" && + isJsSourceFile(ts.getSourceFileOfNode(call)) && + cb.type.kind === "func" && + cb.type.ret.kind !== "promise" && + cb.type.ret.kind !== "void" && + (cb.type.ret.kind === "dyn" || lowerer.dynConvertible(cb.type.ret)) + ) { + resultT = { kind: "promise", inner: DYN }; + } if (resultT?.kind !== "promise") { lowerer.noLowering( "then with this handler's result type", @@ -10939,3 +10970,20 @@ function fenceClassCopyIntoFreshObject( `Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`, ); } + +/** True when an untyped JavaScript receiver names a binding whose declaring + * file assigns it an own `toString` (the TypeScript namespace downlevel + * `errorUtil.toString = (message) => ...` in zod): its calls answer what + * that function returns, not Object.prototype.toString's string. */ +function jsAssignsOwnToString(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (!ts.isIdentifier(receiver)) return false; + let sym = lowerer.checker.getSymbolAtLocation(receiver); + if (sym !== undefined && (sym.flags & ts.SymbolFlags.Alias) !== 0) + sym = lowerer.checker.getAliasedSymbol(sym); + const decl = sym?.valueDeclaration ?? sym?.declarations[0]; + if (decl === undefined) return false; + const sf = ts.getSourceFileOfNode(decl); + if (!isJsSourceFile(sf)) return false; + const name = ts.symbolName(sym!); + return new RegExp(`\\b${name.replace(/[$]/g, "\\$")}\\.toString\\s*=[^=]`).test(sf.text); +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json new file mode 100644 index 000000000..eacdfc1d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/js-own-tostring/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/js-own-tostring/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json new file mode 100644 index 000000000..c9ff338fa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/js-own-tostring/main.ts b/tests/fixtures/npm/cases/js-own-tostring/main.ts new file mode 100644 index 000000000..279e2fd66 --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/main.ts @@ -0,0 +1,14 @@ +// A JavaScript package (the TypeScript namespace downlevel zod uses) whose +// namespace object assigns its own `toString` returning string | undefined; +// Number.parseInt with one argument; and a `then` whose handler result the +// declared union types (zod's ParseStatus.mergeArray pattern). +import { check, direct, mergeLater, wholePart } from "nsutil"; + +console.log(String(direct("a")), String(direct({ message: "b" })), String(direct(undefined))); +console.log(JSON.stringify(check(undefined)), JSON.stringify(check("m"))); +console.log(wholePart(1.23), wholePart(0.35), wholePart(-4.5), wholePart(12)); +async function main(): Promise { + console.log(JSON.stringify(await mergeLater([Promise.resolve(1), Promise.resolve("two")], { ok: true }))); + console.log(JSON.stringify(await mergeLater([Promise.resolve(3), Promise.resolve("four")], { ok: false }))); +} +void main(); diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.d.ts @@ -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/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js new file mode 100644 index 000000000..1b48d927b --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/errorUtil.js @@ -0,0 +1,5 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts new file mode 100644 index 000000000..f4eea833a --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts @@ -0,0 +1,6 @@ +import { errorUtil } from "./errorUtil.js"; +export declare function check(message?: errorUtil.ErrMessage): { kind: "int"; message?: string }; +export declare function direct(message?: errorUtil.ErrMessage): string | undefined; +export declare function wholePart(value: number): number; +export type Merged = { status: "valid"; value: unknown[] } | { status: "aborted" }; +export declare function mergeLater(items: Promise[], status: { ok: boolean }): Promise; diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js new file mode 100644 index 000000000..272a4631c --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js @@ -0,0 +1,14 @@ +import { errorUtil } from "./errorUtil.js"; +export function check(message) { + return { kind: "int", message: errorUtil.toString(message) }; +} +export function direct(message) { + return errorUtil.toString(message); +} +export function wholePart(value) { + return Number.parseInt(String(value).replace(".", "")) + Number.parseInt("0x1f"); +} +export function mergeLater(items, status) { + return Promise.all(items).then((results) => status.ok ? { status: "valid", value: results } : INVALID); +} +const INVALID = Object.freeze({ status: "aborted" }); diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json new file mode 100644 index 000000000..a2757b8a1 --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/package.json @@ -0,0 +1 @@ +{ "name": "nsutil", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts" } diff --git a/tests/fixtures/npm/cases/js-own-tostring/package.json b/tests/fixtures/npm/cases/js-own-tostring/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/js-own-tostring/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3/main.ts b/tests/fixtures/npm/cases/zod3/main.ts new file mode 100644 index 000000000..d7f1a433a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/main.ts @@ -0,0 +1,115 @@ +// zod 3.25 (the real package, trimmed to its v3 ESM build and types) compiled +// natively: schemas built at module top level, parse and safeParse across the +// common schema kinds, number checks, issue paths, codes and messages, and +// async refinements through parseAsync/safeParseAsync. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; +const issues = (result: z.SafeParseReturnType): string => + result.success + ? "ok" + : result.error.issues.map((i) => `${i.path.join(".")}:${i.code}:${i.message}`).join(" | "); + +const Region = z.enum(["east", "west", "north"]); +const Job = z.object({ + id: z.string().min(3).max(32), + region: Region, + attempts: z.number().int().nonnegative().default(0), + tags: z.record(z.string()).optional(), + files: z.array(z.object({ path: z.string(), mode: z.number() })), + kind: z.union([z.literal("static"), z.literal("worker")]), + meta: z.object({ owner: z.string().nullable() }).partial(), +}); +type Job = z.infer; + +const job: Job = Job.parse({ + id: "job_1", + region: "east", + files: [{ path: "a.js", mode: 420 }], + kind: "worker", + meta: { owner: null }, +}); +show("job", job); +show("bad job", issues(Job.safeParse({ id: "x", region: "south", files: [{ path: 1 }], kind: "edge" }))); + +// The common schema kinds. +show("number", z.number().parse(3)); +show("default", z.number().default(0).parse(undefined)); +show("optional", z.string().optional().parse(undefined) ?? "undefined"); +show("nullable", z.string().nullable().parse(null)); +show("record", z.record(z.string()).parse({ a: "b" })); +show("array", z.array(z.string()).parse(["a"])); +show("union", z.union([z.literal("a"), z.literal("b")]).parse("b")); +show("partial", z.object({ a: z.string() }).partial().parse({})); +show("discriminated", z.discriminatedUnion("t", [z.object({ t: z.literal("x") })]).parse({ t: "x" })); +show("transform", z.string().transform((s) => s.length).parse("abc")); +show("wrong type", issues(z.object({ a: z.number() }).safeParse({ a: "x" }))); +show("email", issues(z.string().email().safeParse("nope"))); +show("boolean", z.boolean().parse(true)); +show("strict", issues(z.object({ a: z.string() }).strict().safeParse({ a: "x", b: 1 }))); +show("regex", z.string().regex(/^a/).parse("abc")); +show("coerce", z.coerce.number().parse("4")); +show("extend", z.object({ a: z.string() }).extend({ b: z.number() }).parse({ a: "x", b: 1 })); +show("refine", issues(z.string().refine((s) => s.length > 1, "short").safeParse("a"))); +show("enum", z.enum(["a", "b"]).parse("a")); +show("literal", z.literal("q").parse("q")); + +// Number checks, passing and failing, with and without messages. +show("int", z.number().int().parse(3)); +show("int fail", issues(z.number().int().safeParse(1.5))); +show("int message", issues(z.number().int("whole numbers only").safeParse(2.5))); +show("min", z.number().min(1).parse(3)); +show("min fail", issues(z.number().min(1, "too low").safeParse(0))); +show("max fail", issues(z.number().max(10).safeParse(11))); +show("gt lt", issues(z.number().gt(0).lt(5).safeParse(5))); +show("positive", issues(z.number().positive().safeParse(0))); +show("nonnegative", issues(z.number().nonnegative().safeParse(-1))); +show("multipleOf", z.number().multipleOf(2).parse(4)); +show("multipleOf fail", issues(z.number().multipleOf(0.1).safeParse(0.35))); +show("multipleOf float", z.number().multipleOf(0.01).parse(1.23)); +show("step", issues(z.number().step(3).safeParse(10))); +show("finite", issues(z.number().finite().safeParse(Infinity))); +show("safe", issues(z.number().safe().safeParse(2 ** 60))); +show("bounds", [z.number().min(2).max(9).minValue, z.number().min(2).max(9).maxValue, z.number().int().isInt]); + +try { + Job.parse({}); +} catch (error) { + show("thrown", error instanceof z.ZodError ? error.issues.length : "other"); +} + +// Async refinements. +const Unique = z.string().refine(async (name) => name !== "taken", { message: "name is taken" }); +const Account = z.object({ + name: Unique, + age: z.number().int().min(18), +}); + +async function main(): Promise { + show("async ok", await Account.parseAsync({ name: "free", age: 30 })); + show("async fail", issues(await Account.safeParseAsync({ name: "taken", age: 12.5 }))); + try { + await Unique.parseAsync("taken"); + } catch (error) { + show("async thrown", error instanceof z.ZodError ? error.issues[0]!.message : "other"); + } + try { + Unique.parse("free"); + } catch (error) { + show("sync on async", (error as Error).message); + } + const Slow = z.number().superRefine(async (n, ctx) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + if (n % 2 !== 0) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "odd" }); + }); + show("superRefine", issues(await Slow.safeParseAsync(3))); + show("transform async", await z.string().transform(async (s) => s.toUpperCase()).parseAsync("up")); + show("array async", issues(await z.array(Unique).safeParseAsync(["a", "taken", "b"]))); + show("array async ok", await z.array(Unique).parseAsync(["x", "y"])); + show("tuple async", issues(await z.tuple([z.string(), Unique]).safeParseAsync(["a", "taken"]))); + show("tuple async ok", await z.tuple([z.number(), Unique]).parseAsync([1, "z"])); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/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/cases/zod3/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @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. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * 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) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * 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(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +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 const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +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"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + 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/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -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/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -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/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +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/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +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/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -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/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -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/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -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/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +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> { + private _cached; + _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/cases/zod3/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3/package.json b/tests/fixtures/npm/cases/zod3/package.json new file mode 100644 index 000000000..cda6a6168 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3/package.json @@ -0,0 +1 @@ +{ "name": "zod3-case", "private": true, "type": "module" } From 45146d42562834b8b138a8a81c4fff049b0c7744 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 05:48:46 -0700 Subject: [PATCH 014/169] Keep declaration order and override placeholders in record spreads A fixed-shape spread into an index-signature record copied its fields alphabetically; they now enter in declaration order, as the object's own keys do. A field whose type does not fit the index slot is valid when a later plain property overrides it ({ ...tags, status: String(tags.status) }): a placeholder of the slot's type holds the key's position until the override writes. New corpus program 6916. Co-Authored-By: Claude Opus 5.5 --- .../lowering/containers/indexed-objects.ts | 49 +++++++++++++++---- ...6916-index-record-spread-overrides.ts.json | 6 +++ .../6916-index-record-spread-overrides.ts | 43 ++++++++++++++++ ...6916-index-record-spread-overrides.ts.json | 1 + 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json create mode 100644 tests/corpus/6916-index-record-spread-overrides.ts create mode 100644 tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index b3faf649e..75d584e4b 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1388,6 +1388,15 @@ export type IndexMergeContributor = * JS own-key order. Explicit values arrive pre-coerced to the target's * value slot. Null when a spread source is outside that matrix (the * caller keeps its fence). */ +/** A value of an index slot's type that holds a key's place until a later + * write replaces it, or null for slot types without a literal. */ +function indexSlotPlaceholder(slot: IrType, loc: SrcLoc): IrExpr | null { + if (slot.kind === "string") return { kind: "strLit", value: "", type: STRING, loc }; + if (slot.kind === "f64") return { kind: "numLit", value: 0, type: slot, loc }; + if (slot.kind === "bool") return { kind: "boolLit", value: false, type: slot, loc }; + return null; +} + export function lowerIndexMergeHelper( lowerer: Lowerer, toId: string, @@ -1410,9 +1419,9 @@ export function lowerIndexMergeHelper( shapeId: string; wrapTag: number; retag: string | null; - fields: Map | null; + fields: Map | null; }[] = []; - for (const c of contributors) { + for (const [ci, c] of contributors.entries()) { // A conditional spread needs the undefined arm as its "absent" // value — a target slot without one can't carry the empty arm. if ( @@ -1426,15 +1435,26 @@ export function lowerIndexMergeHelper( if (!from || from.tuple) return null; if (!from.indexValue) { if (from.fields.some((f) => f.name.startsWith("%"))) return null; - const fields = new Map(); + const fields = new Map(); for (const ff of from.fields) { const lift = typeEquals(ff.type, tIv) ? ({ how: "copy" } as WidthLift) : tIv.kind === "dyn" && (ff.type.kind === "dyn" || lowerer.dynConvertible(ff.type)) ? "dyn" : lowerer.widthLiftPlan(ff.type, tIv); - if (lift === null) return null; - fields.set(ff.name, lift); + if (lift !== null) { + fields.set(ff.name, lift); + continue; + } + // A field a later plain property overrides (`{ ...tags, status: + // String(tags.status) }`): its value never survives, but its key + // keeps the spread's position, so a placeholder of the slot's + // type holds the place until the override writes. + const overridden = contributors + .slice(ci + 1) + .some((later) => later.kind === "field" && later.name === ff.name); + if (!overridden || indexSlotPlaceholder(tIv, loc) === null) return null; + fields.set(ff.name, "placeholder"); } srcPlans.push({ shapeId: c.shapeId, wrapTag: -1, retag: null, fields }); continue; @@ -1569,7 +1589,16 @@ export function lowerIndexMergeHelper( // own planned lift (absent optionals skip — the unset convention); // no overflow exists to walk. if (plan.fields !== null) { - for (const ff of from.fields) { + // Keys enter in DECLARATION order, as the object's own keys do + // (declaredOrder omits internal '%'-fields, which never copy). + lowerer.keyOrder.observed.add(from.id); + const orderedFields = from.declaredOrder + ? from.declaredOrder.flatMap((n) => { + const f = from.fields.find((x) => x.name === n); + return f ? [f] : []; + }) + : from.fields; + for (const ff of orderedFields) { const lift = plan.fields.get(ff.name)!; const raw: IrExpr = { kind: "recordGet", @@ -1586,9 +1615,11 @@ export function lowerIndexMergeHelper( shapeId: toId, key: { kind: "strLit", value: ff.name, type: STRING, loc }, value: - lift === "dyn" - ? { kind: "dynFrom", value: raw, type: DYN, loc } - : lowerer.applyWidthLift(lift, raw, tIv, loc), + lift === "placeholder" + ? indexSlotPlaceholder(tIv, loc)! + : lift === "dyn" + ? { kind: "dynFrom", value: raw, type: DYN, loc } + : lowerer.applyWidthLift(lift, raw, tIv, loc), overflowOnly: true, loc, }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json new file mode 100644 index 000000000..db4bc557f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6916-index-record-spread-overrides.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6916-index-record-spread-overrides.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6916-index-record-spread-overrides.ts b/tests/corpus/6916-index-record-spread-overrides.ts new file mode 100644 index 000000000..094b17f02 --- /dev/null +++ b/tests/corpus/6916-index-record-spread-overrides.ts @@ -0,0 +1,43 @@ +// Spreading a fixed-shape record into an index-signature record: the +// spread's keys enter in declaration order, and a field whose type does +// not fit the index slot is valid when a later property overrides it +// (its key keeps the spread's position, the override supplies the value). +interface FailureTags { + phase: string; + httpStatus: number; + retried: boolean; + region: string; +} +interface Counts { + zeta: number; + alpha: number; + mid: number; +} + +function emit(name: string, tags: { [key: string]: string }): void { + const parts: string[] = []; + for (const key of Object.keys(tags)) parts.push(key + ":" + tags[key]); + console.log(name, parts.join(","), JSON.stringify(tags)); +} + +const tags: FailureTags = { phase: "fetch", httpStatus: 503, retried: true, region: "iad1" }; +for (let attempt = 1; attempt <= 2; attempt++) { + emit("attempt_failed", { + ...tags, + attempt: String(attempt), + httpStatus: String(tags.httpStatus), + retried: tags.retried ? "yes" : "no", + }); +} + +const base: Record = { team: "core", owner: "infra" }; +emit("queue", { ...base, owner: "builds", shard: "7" }); +emit("size", { ...base }); + +const counts: Counts = { zeta: 3, alpha: 1, mid: 2 }; +const scores: Record = { ...counts, alpha: 10 }; +console.log(Object.keys(scores).join(","), JSON.stringify(scores)); +const labels: Record = { first: "a", ...{ zeta: "z", alpha: "a" }, last: "l" }; +console.log(Object.entries(labels).map(([k, v]) => k + "=" + v).join(" ")); +const flags: { [key: string]: boolean } = { ...counts, mid: true, zeta: false, alpha: true }; +console.log(JSON.stringify(flags)); diff --git a/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json b/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6916-index-record-spread-overrides.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } From 12563f720e0cee6474763a371ac360b1f58331b2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:58:53 -0500 Subject: [PATCH 015/169] Compile conditional spreads of several properties and --- .../ambient/scriptc-node-fallback.d.ts | 24 +- .../builtins/child-process-methods.ts | 65 +++-- .../frontend/lowering/builtins/promises.ts | 97 ++++++- .../src/frontend/lowering/builtins/url.ts | 25 +- .../lowering/coercions/structural-plans.ts | 58 +++- .../frontend/lowering/coercions/structural.ts | 74 ++++- .../lowering/containers/array-callbacks.ts | 271 ++++++++++++++++++ .../lowering/expressions/array-literals.ts | 35 +++ .../lowering/expressions/object-literals.ts | 180 ++++++++---- .../src/frontend/lowering/lower-exprs.ts | 3 + .../src/frontend/lowering/lower-stmts.ts | 169 ++++++++++- .../compiler/src/frontend/lowering/lowerer.ts | 26 +- .../src/frontend/lowering/width-lift.ts | 11 + packages/compiler/src/frontend/type-mapper.ts | 21 ++ .../7020-conditional-spread-fields.ts.json | 6 + .../7021-promise-all-uniform-tuple.ts.json | 6 + .../7022-promise-all-dynamic-arrays.ts.json | 6 + .../corpus/7023-union-object-patterns.ts.json | 6 + ...024-async-return-fresh-collections.ts.json | 6 + .../7025-child-listener-chaining.ts.json | 6 + .../corpus/7026-url-object-patterns.ts.json | 6 + ...027-search-params-conditional-init.ts.json | 6 + .../corpus/7028-non-empty-rest-tuples.ts.json | 6 + .../corpus/7029-flatmap-union-results.ts.json | 6 + ...3-any-fields-into-declared-records.ts.json | 6 + .../7034-optional-field-union-routes.ts.json | 6 + ...engine-1123-any-conditional-spread.ts.json | 6 - .../corpus/7020-conditional-spread-fields.ts | 137 +++++++++ .../corpus/7021-promise-all-uniform-tuple.ts | 46 +++ .../corpus/7022-promise-all-dynamic-arrays.ts | 41 +++ tests/corpus/7023-union-object-patterns.ts | 63 ++++ .../7024-async-return-fresh-collections.ts | 48 ++++ tests/corpus/7025-child-listener-chaining.ts | 42 +++ tests/corpus/7026-url-object-patterns.ts | 27 ++ .../7027-search-params-conditional-init.ts | 21 ++ tests/corpus/7028-non-empty-rest-tuples.ts | 38 +++ tests/corpus/7029-flatmap-union-results.ts | 31 ++ .../7033-any-fields-into-declared-records.ts | 28 ++ .../7034-optional-field-union-routes.ts | 26 ++ tests/diagnostics/destructuring-fences.ts | 6 +- ...ired-engine-1123-any-conditional-spread.ts | 49 ---- .../__snapshots__/child-process.ts.txt | 2 +- .../__snapshots__/destructuring-fences.ts.txt | 16 +- ...-engine-1123-any-conditional-spread.ts.txt | 6 - .../__snapshots__/spread-fences.ts.txt | 2 +- .../7020-conditional-spread-fields.ts.json | 1 + .../7021-promise-all-uniform-tuple.ts.json | 1 + .../7022-promise-all-dynamic-arrays.ts.json | 1 + .../7023-union-object-patterns.ts.json | 1 + ...024-async-return-fresh-collections.ts.json | 1 + .../7025-child-listener-chaining.ts.json | 1 + .../7026-url-object-patterns.ts.json | 1 + ...027-search-params-conditional-init.ts.json | 1 + .../7028-non-empty-rest-tuples.ts.json | 1 + .../7029-flatmap-union-results.ts.json | 1 + ...3-any-fields-into-declared-records.ts.json | 1 + .../7034-optional-field-union-routes.ts.json | 1 + 57 files changed, 1609 insertions(+), 169 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json create mode 100644 tests/corpus/7020-conditional-spread-fields.ts create mode 100644 tests/corpus/7021-promise-all-uniform-tuple.ts create mode 100644 tests/corpus/7022-promise-all-dynamic-arrays.ts create mode 100644 tests/corpus/7023-union-object-patterns.ts create mode 100644 tests/corpus/7024-async-return-fresh-collections.ts create mode 100644 tests/corpus/7025-child-listener-chaining.ts create mode 100644 tests/corpus/7026-url-object-patterns.ts create mode 100644 tests/corpus/7027-search-params-conditional-init.ts create mode 100644 tests/corpus/7028-non-empty-rest-tuples.ts create mode 100644 tests/corpus/7029-flatmap-union-results.ts create mode 100644 tests/corpus/7033-any-fields-into-declared-records.ts create mode 100644 tests/corpus/7034-optional-field-union-routes.ts delete mode 100644 tests/diagnostics/retired-engine-1123-any-conditional-spread.ts delete mode 100644 tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt create mode 100644 tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json create mode 100644 tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json create mode 100644 tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json create mode 100644 tests/harness/corpus-costs/7023-union-object-patterns.ts.json create mode 100644 tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json create mode 100644 tests/harness/corpus-costs/7025-child-listener-chaining.ts.json create mode 100644 tests/harness/corpus-costs/7026-url-object-patterns.ts.json create mode 100644 tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json create mode 100644 tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json create mode 100644 tests/harness/corpus-costs/7029-flatmap-union-results.ts.json create mode 100644 tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json create mode 100644 tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..561dcc124 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3554,20 +3554,20 @@ declare module "child_process" { export interface ChildProcess extends Disposable { ref(): void; - on(event: "spawn", listener: () => void): void; - once(event: "spawn", listener: () => void): void; + on(event: "spawn", listener: () => void): this; + once(event: "spawn", listener: () => void): this; /* The exit listener may also take Node's second parameter — the * terminating signal's name, null for a normal exit. */ - on(event: "exit", listener: (code: number | null, signal: string | null) => void): void; - on(event: "close", listener: (code: number | null, signal: string | null) => void): void; - on(event: "error", listener: (err: Error) => void): void; - once(event: "exit", listener: (code: number | null, signal: string | null) => void): void; - once(event: "close", listener: (code: number | null, signal: string | null) => void): void; - once(event: "error", listener: (err: Error) => void): void; - on(event: "message", listener: (message: any) => void): void; - on(event: "disconnect", listener: () => void): void; - once(event: "message", listener: (message: any) => void): void; - once(event: "disconnect", listener: () => void): void; + on(event: "exit", listener: (code: number | null, signal: string | null) => void): this; + on(event: "close", listener: (code: number | null, signal: string | null) => void): this; + on(event: "error", listener: (err: Error) => void): this; + once(event: "exit", listener: (code: number | null, signal: string | null) => void): this; + once(event: "close", listener: (code: number | null, signal: string | null) => void): this; + once(event: "error", listener: (err: Error) => void): this; + on(event: "message", listener: (message: any) => void): this; + on(event: "disconnect", listener: () => void): this; + once(event: "message", listener: (message: any) => void): this; + once(event: "disconnect", listener: () => void): this; /* The lifecycle members, Node's exact shapes: pid is undefined exactly * when the spawn failed; exitCode is null while running, the code * after a normal exit, null for a signal death, and -errno once a diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index af7270ea9..2e0f305a0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -1,4 +1,4 @@ -import { boolLit, strLit } from "../../../ir/build.js"; +import { boolLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; import { type Lowerer } from "../lowerer.js"; @@ -93,32 +93,47 @@ export function lowerChildMethodCall( '"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)', ); } - if (!ts.isExpressionStatement(call.parent)) { - lowerer.unsupported( - "SC1090", - call, - `chaining child.${name}(...) (the result is void here — register each listener as its own statement)`, - ); - } - const receiver = lowerer.lowerExpr(access.expression); + // `child.on(...)` returns the child: a chained call or a stored result + // reads the receiver, which evaluates once, before the listener. + let receiver = lowerer.lowerExpr(access.expression); + const chained = ts.isExpressionStatement(call.parent) + ? null + : { + local: lowerer.declareHiddenLocal("%childListenerTarget", receiver.type), + init: receiver, + }; + if (chained) receiver = varRef(chained.local.id, chained.local.type, loc); + const registered = (registration: IrExpr): IrExpr => + chained === null + ? registration + : { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: chained.local.id, init: chained.init, loc }, + { kind: "exprStmt", expr: registration, loc }, + ], + result: varRef(chained.local.id, chained.local.type, loc), + type: chained.local.type, + loc, + }; const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc }; if (event === "message") { - return { + return registered({ kind: "libCall", fn: "child.onMessage", args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, - }; + }); } if (event === "disconnect") { - return { + return registered({ kind: "libCall", fn: "child.onDisconnect", args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, - }; + }); } const listenerNode = stripTypeCasts(call.arguments.nodes[1]!); if ( @@ -163,7 +178,7 @@ export function lowerChildMethodCall( call.arguments.nodes[1]!, ); } - return { + return registered({ kind: "libCall", fn: "child.onDyn", args: [ @@ -173,7 +188,7 @@ export function lowerChildMethodCall( ], type: VOID, loc, - }; + }); } if (event === "spawn") { if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { @@ -182,7 +197,13 @@ export function lowerChildMethodCall( call.arguments.nodes[1]!, ); } - return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc }; + return registered({ + kind: "libCall", + fn: "child.onSpawn", + args: [receiver, cb], + type: VOID, + loc, + }); } if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) { lowerer.unsupported( @@ -243,13 +264,13 @@ export function lowerChildMethodCall( `${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`, ); } - return { + return registered({ kind: "libCall", fn: event === "close" ? "child.onClose" : "child.onExit", args: [receiver, cb], type: VOID, loc, - }; + }); } if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( @@ -258,7 +279,13 @@ export function lowerChildMethodCall( `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } - return { kind: "libCall", fn: "child.onError", args: [receiver, cb], type: VOID, loc }; + return registered({ + kind: "libCall", + fn: "child.onError", + args: [receiver, cb], + type: VOID, + loc, + }); } // child.kill(signal?) — Node's semantics exactly: the name resolves // through Node's signal table (unknown names throw the ERR_UNKNOWN_SIGNAL diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5..129ac73a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -9,6 +9,7 @@ import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, DYN, + F64, type IrExpr, type IrFunction, type IrStmt, @@ -376,8 +377,14 @@ export function lowerPromiseStaticCall( ); } const entries = lowerer.lowerExpr(argNode); - if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn") - return checkedPromiseAll(lowerer, entries, loc); + if (entries.type.kind === "dyn") return checkedPromiseAll(lowerer, entries, loc); + // An 'any'-typed argument that holds a typed value (`const xs: any = + // []`): the combinator reads it as the runtime value it is. + if ( + (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 && + lowerer.dynConvertible(entries.type) + ) + return checkedPromiseAll(lowerer, lowerer.coerceToExpected(entries, DYN), loc); // loadPlugin(p)))` — the loadPlugins shape, where the checker // spells `any[]`): the ENGINE's own Promise.all runs over the @@ -845,3 +852,89 @@ function lowerTypedSettledCombinator( } return { kind: "intrinsic", name: "promise.allSettled", args: [entries], type: resultT, loc }; } + +/** `const pair = await Promise.all([p, q])` where every entry is the same + * Promise and the binding's type is the checker's tuple `[T, T]`: the + * uniform entries settle through one runtime combinator into a T[] (the + * same combinator and turn as the array form), and the binding receives + * the tuple of its elements. Null for every other await. */ +export function lowerAwaitedUniformPromiseAll( + lowerer: Lowerer, + expr: ts.AwaitExpression, +): IrExpr | null { + let operand: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if ( + !ts.isCallExpression(operand) || + operand.questionDotToken || + !ts.isPropertyAccessExpression(operand.expression) || + operand.arguments.nodes.length !== 1 || + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) + return null; + const argNode = operand.arguments.nodes[0]!; + if ( + !ts.isArrayLiteralExpression(argNode) || + argNode.elements.nodes.length === 0 || + argNode.elements.nodes.some(ts.isSpreadElement) + ) + return null; + let user: ts.Node = expr; + while (ts.isParenthesizedExpression(user.parent)) user = user.parent; + const decl = user.parent; + if (!ts.isVariableDeclaration(decl) || decl.initializer !== user || !ts.isIdentifier(decl.name)) + return null; + if (lowerer.stdlibGlobalMember(operand.expression, "Promise") !== "all") return null; + const tuple = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if (tuple?.kind !== "record") return null; + const shape = lowerer.shapes.get(tuple.shapeId); + if (!shape?.tuple || shape.fields.length !== argNode.elements.nodes.length) return null; + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); + const first = elems[0]!.type; + if ( + first.kind !== "promise" || + first.inner.kind === "void" || + !elems.every((e) => typeEquals(e.type, first)) || + !shape.fields.every((f) => typeEquals(f.type, first.inner)) + ) + return null; + const loc = locOf(expr); + const values = arrayOf(first.inner); + const local = lowerer.declareHiddenLocal("%allValues", values); + const all: IrExpr = { + kind: "intrinsic", + name: "promise.all", + args: [{ kind: "arrayLit", elems, type: arrayOf(first), loc }], + type: { kind: "promise", inner: values }, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: local.id, + init: { kind: "awaitExpr", value: all, type: values, loc }, + loc, + }, + ], + // Shape fields are sorted by name; a tuple field's name is its position. + result: { + kind: "recordLit", + fields: shape.fields.map((f) => ({ + name: f.name, + value: { + kind: "arrayGet", + arr: varRef(local.id, values, loc), + index: { kind: "numLit", value: Number(f.name), type: F64, loc }, + type: f.type, + loc, + }, + })), + type: tuple, + loc, + }, + type: tuple, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 2a77fa85c..8619b4997 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -195,7 +195,30 @@ export function lowerSearchParamsNew( ); } const arg = args[0]; - if (arg === undefined || (ts.isIdentifier(arg) && arg.text === "undefined")) { + if (arg === undefined) { + return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }; + } + return lowerSearchParamsInit(lowerer, arg, loc); +} + +/** The URLSearchParams built from one init expression. A conditional + * init (`new URLSearchParams(c ? { a: "1" } : {})`) evaluates its + * condition first and builds from the arm it selects, so each arm may be + * any supported init shape. */ +function lowerSearchParamsInit(lowerer: Lowerer, arg: ts.Expression, loc: SrcLoc): IrExpr { + let inner = arg; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + if (ts.isConditionalExpression(inner)) { + return { + kind: "ternary", + cond: lowerer.lowerCondition(inner.condition), + then: lowerSearchParamsInit(lowerer, inner.whenTrue, loc), + else_: lowerSearchParamsInit(lowerer, inner.whenFalse, loc), + type: SEARCH_PARAMS_T, + loc, + }; + } + if (ts.isIdentifier(arg) && arg.text === "undefined") { return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }; } // The object-literal init: `{ a: "1", b: "2" }` appends pairs in diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 53b0748b4..f42663a13 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -1,7 +1,7 @@ import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js"; import type { WidthLift } from "../width-lift.js"; import type { IrType } from "../../../ir/ir.js"; -import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; +import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; import { type ClassInfo, findMethodOn, findGenericMethodOn } from "../lower-classes.js"; import type { Lowerer } from "../lowerer.js"; @@ -21,6 +21,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width if (dst.kind === "dyn" && src.kind !== "dyn" && lowerer.dynConvertible(src)) { return { how: "dynIn" }; } + // An 'unknown' (dyn) SOURCE field into a typed slot — a field TypeScript + // types `any` (a JSON.parse read), which the checker lets into any + // declared field type: the checked extraction, the same dynCheck a + // typed binding initialized from that value applies. + if ( + src.kind === "dyn" && + dst.kind !== "dyn" && + dst.kind !== "union" && + canDynCheckTo( + dst, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return { how: "dynOut" }; + } if (dst.kind === "union") { if (src.kind === "union") { if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" }; @@ -108,6 +124,10 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width return { how: "discriminantRoute" }; } } + if (selected.length === 2 && src.kind === "record") { + const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!); + if (route) return route; + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } @@ -202,6 +222,42 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width return null; } +/** Two record arms a source record fits, where one arm declares a field + * the other lacks and the source types that field `T | undefined` + * (`{ count, errors: errs.length ? errs : undefined }` into `{ count; + * errors: string[] } | { count }`): the field's value picks the arm at + * run time. Null for any other pair. */ +function presenceRoute( + lowerer: Lowerer, + sourceId: string, + a: { tag: number; arm: IrType }, + b: { tag: number; arm: IrType }, +): WidthLift | null { + const source = lowerer.shapes.get(sourceId); + if (!source || source.tuple || source.indexValue || a.arm.kind !== "record") return null; + if (b.arm.kind !== "record") return null; + const shapeA = lowerer.shapes.get(a.arm.shapeId); + const shapeB = lowerer.shapes.get(b.arm.shapeId); + if (!shapeA || !shapeB) return null; + const optional = (t: IrType): boolean => + t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0; + for (const [present, absent, presentShape, absentShape] of [ + [a, b, shapeA, shapeB], + [b, a, shapeB, shapeA], + ] as const) { + const extra = presentShape.fields.filter( + (f) => !absentShape.fields.some((g) => g.name === f.name), + ); + if (extra.length !== 1 || optional(extra[0]!.type)) continue; + if (absentShape.fields.some((g) => !presentShape.fields.some((f) => f.name === g.name))) + continue; + const field = source.fields.find((f) => f.name === extra[0]!.name); + if (!field || !optional(field.type)) continue; + return { how: "presenceRoute", field: field.name, present, absent }; + } + return null; +} + /** True for the unit-only element types (`(null | undefined)[]`, the * `readonly []` mapping): a union whose every arm is a unit. */ export function unitOnlyElem(lowerer: Lowerer, t: IrType): boolean { diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 5b5e283b3..c3b4ad906 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -6,7 +6,7 @@ import { buildArrayConversion } from "./builders.js"; import type { WidthLift } from "../width-lift.js"; import { InternalCompilerError } from "../../../errors.js"; import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js"; -import { BOOL, DYN, F64, STRING } from "../../../ir/ir.js"; +import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js"; import { typeKey } from "../../type-mapper.js"; import { findStaticOn, findGenericStaticOn } from "../lower-classes.js"; import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js"; @@ -155,6 +155,76 @@ export function applyWidthLift( loc, }; } + case "presenceRoute": { + if (dst.kind !== "union" || value.type.kind !== "record") + throw new InternalCompilerError("lowerer bug: presenceRoute lift shape"); + const sourceType = value.type; + const fieldType = lowerer.shapes + .get(sourceType.shapeId) + ?.fields.find((f) => f.name === lift.field)?.type; + if (fieldType?.kind !== "union") + throw new InternalCompilerError("lowerer bug: presenceRoute field shape"); + // One helper per pair: the conversion reads the source twice. + const key = `presence:${typeKey(sourceType)}:${dst.unionId}:${lift.field}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = `%union.presence.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, helper); + const source = (): IrExpr => varRef("v.0", sourceType, loc); + const arm = (route: { tag: number; arm: IrType }): IrExpr => { + const inner = lowerer.widthLiftPlan(sourceType, route.arm); + if (!inner) + throw new InternalCompilerError( + "lowerer bug: planned presenceRoute arm stopped lifting", + ); + return { + kind: "unionWrap", + unionId: dst.unionId, + tag: route.tag, + value: lowerer.applyWidthLift(inner, source(), route.arm, loc), + type: dst, + loc, + }; + }; + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "v.0", name: "v", type: sourceType }], + returnType: dst, + locals: [{ id: "v.0", name: "v", type: sourceType, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: fieldType.unionId, + tag: lowerer.armTag(fieldType.unionId, UNDEFINED_T), + value: { + kind: "recordGet", + obj: source(), + shapeId: sourceType.shapeId, + field: lift.field, + type: fieldType, + loc, + }, + negated: false, + type: BOOL, + loc, + }, + then: arm(lift.absent), + else_: arm(lift.present), + type: dst, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: helper, args: [value], type: dst, loc }; + } case "width": { if (dst.kind !== "record" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: width lift shape"); @@ -220,6 +290,8 @@ export function applyWidthLift( throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot"); return { kind: "dynFrom", value, type: DYN, loc }; } + case "dynOut": + return { kind: "dynCheck", value, type: dst, loc }; case "upcast": { if (dst.kind !== "object" || value.type.kind !== "object") throw new InternalCompilerError("lowerer bug: upcast lift shape"); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 612c47ea0..3de1a4e6b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1501,6 +1501,19 @@ export function lowerArrayFlatMapCall( } if (fnRet.kind === "union") { const def = lowerer.unions.get(fnRet.unionId); + const plan = def?.arms.some((a) => a.kind === "array") + ? flatMapUnionPlan(lowerer, call, argNode, fnRet) + : null; + if (plan) { + const helper = flatMapUnionHelper(lowerer, elem, fnRet, arity, plan, loc, present); + return { + kind: "call", + callee: helper, + args: [receiver, fnArg], + type: arrayOf(plan.outElem), + loc, + }; + } if (def?.arms.some((a) => a.kind === "array")) { lowerer.unsupported( "SC1090", @@ -1566,6 +1579,264 @@ export function lowerArrayFlatMapCall( }; } +/** How each arm of a flatMap callback's union result contributes to the + * output: an array arm appends its elements, an array arm the checker + * types `never[]` (an empty literal such as `s ? s : []`) appends nothing, + * and any other arm appends itself. */ +interface FlatMapUnionPlan { + outElem: IrType; + arms: ("spread" | "empty" | "value")[]; +} + +/** The plan for a callback returning `T | U[]`: every non-array arm and + * every array arm's element must convert into the result's element type + * without a check. Null when an arm does not. */ +function flatMapUnionPlan( + lowerer: Lowerer, + call: ts.CallExpression, + argNode: ts.Expression, + fnRet: IrType & { kind: "union" }, +): FlatMapUnionPlan | null { + const out = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (out?.kind !== "array") return null; + const outElem = out.elem; + const arms = lowerer.unions.get(fnRet.unionId)?.arms; + if (!arms) return null; + // The `never[]` constituents of the callback's checked result. + const emptyArms: IrType[] = []; + const signature = lowerer.checker.getCallSignatures(lowerer.typeOf(argNode))[0]; + if (signature) { + const ret = lowerer.checker.getReturnTypeOfSignature(signature); + for (const part of ret.isUnionType() ? ts.constituentTypes(ret) : [ret]) { + if (!lowerer.checker.isArrayType(part)) continue; + const [element] = lowerer.checker.getTypeArguments(part as ts.TypeReference); + if (element === undefined || (element.flags & ts.TypeFlags.Never) === 0) continue; + const mapped = lowerer.mapTypeOf(part); + if (mapped) emptyArms.push(mapped); + } + } + const converts = (t: IrType): boolean => { + if (typeEquals(t, outElem)) return true; + if (outElem.kind !== "union") return false; + return lowerer.armTag(outElem.unionId, t) >= 0; + }; + const plan: FlatMapUnionPlan["arms"] = []; + for (const arm of arms) { + if (arm.kind === "array" && emptyArms.some((e) => typeEquals(e, arm))) plan.push("empty"); + else if (arm.kind === "array" && converts(arm.elem)) plan.push("spread"); + else if (arm.kind !== "array" && converts(arm)) plan.push("value"); + else return null; + } + return { outElem, arms: plan }; +} + +/** The flatMap loop over a union-returning callback: each result tests + * its arm and appends its elements, nothing, or itself (Node's + * IsArray decision per value). */ +function flatMapUnionHelper( + lowerer: Lowerer, + elem: IrType, + fnRet: IrType & { kind: "union" }, + arity: number, + plan: FlatMapUnionPlan, + loc: SrcLoc, + present: boolean, +): string { + const key = `flatMapUnion:${typeKey(elem)}:${typeKey(fnRet)}:${typeKey(plan.outElem)}:${arity}${present ? ":present" : ""}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const arrT = arrayOf(elem); + const outT = arrayOf(plan.outElem); + const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet); + const arms = lowerer.unions.get(fnRet.unionId)!.arms; + const outRef = (): IrExpr => varRef("out.0", outT, loc); + const resultRef = (): IrExpr => varRef("r.0", fnRet, loc); + const outLength = (): IrExpr => ({ + kind: "arrIntrinsic", + method: "length", + receiver: outRef(), + args: [], + type: F64, + loc, + }); + const locals: IrLocal[] = [ + ...arrayCallbackLocals(arrT, fnT), + { id: "out.0", name: "out", type: outT, mutable: false }, + { id: "r.0", name: "r", type: fnRet, mutable: false }, + ]; + const armBody = (tag: number): IrStmt[] => { + const arm = arms[tag]!; + const narrowed: IrExpr = { + kind: "unionNarrow", + unionId: fnRet.unionId, + tag, + value: resultRef(), + type: arm, + loc, + }; + if (plan.arms[tag] === "empty") return []; + if (plan.arms[tag] === "value") { + return [ + arrayValueStore( + lowerer, + outRef(), + outLength(), + lowerer.coerceToExpected(narrowed, plan.outElem), + plan.outElem, + loc, + ), + ]; + } + const innerT = arm as IrType & { kind: "array" }; + const listId = `list.${tag}`; + const lengthId = `m.${tag}`; + const indexId = `j.${tag}`; + const itemId = `inner.${tag}`; + locals.push( + { id: listId, name: "list", type: innerT, mutable: false }, + { id: lengthId, name: "m", type: F64, mutable: false }, + { id: indexId, name: "j", type: F64, mutable: true }, + { id: itemId, name: "inner", type: arrayValueType(lowerer, innerT.elem), mutable: false }, + ); + const list = (): IrExpr => varRef(listId, innerT, loc); + const index = (): IrExpr => varRef(indexId, F64, loc); + return [ + { kind: "varDecl", localId: listId, init: narrowed, loc }, + { + kind: "varDecl", + localId: lengthId, + init: { + kind: "arrIntrinsic", + method: "length", + receiver: list(), + args: [], + type: F64, + loc, + }, + loc, + }, + { + kind: "for", + init: { kind: "varDecl", localId: indexId, init: numLit(0, loc), loc }, + cond: { + kind: "bin", + op: "<", + left: index(), + right: varRef(lengthId, F64, loc), + type: BOOL, + loc, + }, + update: { + kind: "assign", + localId: indexId, + value: { kind: "bin", op: "+", left: index(), right: numLit(1, loc), type: F64, loc }, + loc, + }, + body: [ + { + kind: "if", + cond: arrayIndexPresent(list(), index(), loc), + then: [ + { + kind: "varDecl", + localId: itemId, + init: arrayValueRead(lowerer, list(), index(), innerT.elem, loc), + loc, + }, + arrayValueStore( + lowerer, + outRef(), + outLength(), + lowerer.coerceToExpected( + varRef(itemId, arrayValueType(lowerer, innerT.elem), loc), + plan.outElem, + ), + plan.outElem, + loc, + ), + ], + else_: null, + loc, + }, + ], + loc, + }, + ]; + }; + let dispatch: IrStmt[] = armBody(arms.length - 1); + for (let tag = arms.length - 2; tag >= 0; tag--) { + dispatch = [ + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: fnRet.unionId, + tag, + value: resultRef(), + negated: false, + type: BOOL, + loc, + }, + then: armBody(tag), + else_: dispatch, + loc, + }, + ]; + } + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: "out.0", + init: { kind: "arrayLit", elems: [], type: outT, loc }, + loc, + }, + arrayLengthDeclaration(arrT, loc), + countedFor(loc, varRef("n.0", F64, loc), () => [ + { + kind: "if", + cond: currentArrayIndexPresent(arrT, loc), + then: [ + { + kind: "varDecl", + localId: "r.0", + init: callArrayCallback( + arrT, + fnT, + fnRet, + arity, + callbackValueRead( + lowerer, + varRef("a.0", arrT, loc), + varRef("i.0", F64, loc), + elem, + present, + loc, + ), + loc, + ), + loc, + }, + ...dispatch, + ], + else_: null, + loc, + }, + ]), + { kind: "return", value: outRef(), loc }, + ]; + lowerer.liftedFns.push({ + name, + params: arrayCallbackParams(arrT, fnT), + returnType: outT, + locals, + body, + loc, + }); + return name; +} + /** The flatMap loop (array-returning callback), from existing IR nodes: * * out = []; n = a.length; diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..e5ccd1176 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -58,6 +58,25 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return false; } +/** The literal is the source of an array destructuring: an assignment's + * right side or a declaration's initializer under an array pattern. */ +function isPatternSource(expr: ts.ArrayLiteralExpression): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isBinaryExpression(parent)) + return ( + parent.right === node && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isArrayLiteralExpression(parent.left) + ); + return ( + ts.isVariableDeclaration(parent) && + parent.initializer === node && + ts.isArrayBindingPattern(parent.name) + ); +} + /** `[a, b, c]`. The element type comes from the contextual type when tsc * has one (`const a: number[] = []`, arguments, nested literals) and from * the literal's own inferred type otherwise. A bare `[]` with no context @@ -107,6 +126,22 @@ export function lowerArrayLiteral( ) mapped = inferred; } + // A destructuring pattern's implied type ends in a rest element + // (`[h, ...t] = [1, 2, 3]` supplies `[number, ...number[]]`), which maps + // to an array. The literal keeps its own fixed tuple layout, read by + // position like any tuple source. + if ( + expected === undefined && + ctxType && + isPatternSource(expr) && + lowerer.checker.isTupleType(ctxType) && + ( + (ctxType as ts.TupleTypeReference).getTarget() as ts.TupleType | undefined + )?.elementFlags?.some((flag) => (flag & ts.ElementFlags.Rest) !== 0) + ) { + const own = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (own?.kind === "record" && lowerer.shapes.get(own.shapeId)?.tuple) mapped = own; + } // A JS literal whose OWN inferred type is never-tainted // (neverTaintedJsType — the evolving `const gb = []`, the mixed // command tuple `['pwd', []]`) carries no element information: route diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 678b375fe..d0edc8e28 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -933,11 +933,11 @@ export function lowerObjectLiteral( // spread's own position (any position: they introduce one fresh // name, collision-fenced below). Everything else stays fenced. const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); - if (cs === "unsupported" || (cs && cs.props.length !== 1)) { + if (cs === "unsupported" && conditionalRecordSpreadOf(lowerer, prop.expression) === null) { lowerer.unsupported( "SC1090", prop, - "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)", + "conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields)", ); } // Spread ORDER is fenced in the field-by-field desugar below, not @@ -1721,64 +1721,71 @@ export function lowerObjectLiteral( if (ts.isSpreadAssignment(prop)) { const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs && cs !== "unsupported") { - // `...(c ? { k: v } : {})` — ONE conditional field at the - // spread's own position: cond evaluates exactly once, `v` only - // when the non-empty arm is taken (ternary arms are lazy), and - // the empty arm holds the interned undefined arm — exactly the - // omitted-optional-field representation, which is also why the - // target field must be optional (tsc types the idiom that way). - const csProp = cs.props[0]!; // single-prop-checked in the fence pass - const name = csProp.name.text; - const fieldType = fieldTypes.get(name); - if (!fieldType) { - if (shape.indexValue) { + // `...(c ? { k: v } : {})` — one conditional field per carried + // property at the spread's own position: cond evaluates exactly + // once (into a hidden local when several fields test it), each + // `v` only when the non-empty arm is taken (ternary arms are + // lazy), in property order, and the empty arm holds the interned + // undefined arm — exactly the omitted-optional-field + // representation, which is also why the target fields must be + // optional (tsc types the idiom that way). + let cond: IrExpr | null = null; + for (const csProp of cs.props) { + const name = csProp.name.text; + const fieldType = fieldTypes.get(name); + if (!fieldType) { + if (shape.indexValue) { + lowerer.unsupported( + "SC1090", + prop, + "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)", + ); + } + shapeMismatch(prop); + } + // An 'unknown' slot holds the checked-dynamic undefined for the + // empty arm, the absent-property read the width copy uses too. + const absent = + lowerer.absentFieldValue(fieldType, locOf(prop)) ?? + (fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null); + if (!absent) { lowerer.unsupported( "SC1090", prop, - "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)", + `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`, ); } - shapeMismatch(prop); + if (fields.some((f) => f.name === name)) { + lowerer.unsupported( + "SC1090", + prop, + `conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`, + ); + } + if (cond === null) { + cond = lowerer.lowerCondition(cs.cond); + if (cs.props.length > 1) cond = capture(cond); + } + const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp; + let v = ts.isPropertyAssignment(csProp) + ? lowerer.lowerExpr(csProp.initializer) + : lowerer.lowerShorthandValue(csProp); + v = lowerer.coerceInto(valueNode, v, fieldType); + if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode)); + conditionalNames.add(name); + orderSegments.push({ kind: "key", name, definite: false }); + fields.push({ + name, + value: { + kind: "ternary", + cond, + then: cs.whenTrue ? v : absent, + else_: cs.whenTrue ? absent : v, + type: fieldType, + loc: locOf(prop), + }, + }); } - // An 'unknown' slot holds the checked-dynamic undefined for the - // empty arm, the absent-property read the width copy uses too. - const absent = - lowerer.absentFieldValue(fieldType, locOf(prop)) ?? - (fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null); - if (!absent) { - lowerer.unsupported( - "SC1090", - prop, - `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`, - ); - } - if (fields.some((f) => f.name === name)) { - lowerer.unsupported( - "SC1090", - prop, - `conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`, - ); - } - const cond = lowerer.lowerCondition(cs.cond); - const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp; - let v = ts.isPropertyAssignment(csProp) - ? lowerer.lowerExpr(csProp.initializer) - : lowerer.lowerShorthandValue(csProp); - v = lowerer.coerceInto(valueNode, v, fieldType); - if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode)); - conditionalNames.add(name); - orderSegments.push({ kind: "key", name, definite: false }); - fields.push({ - name, - value: { - kind: "ternary", - cond, - then: cs.whenTrue ? v : absent, - else_: cs.whenTrue ? absent : v, - type: fieldType, - loc: locOf(prop), - }, - }); continue; } // `{ ...base, ... }` — field-by-field copy of a known record @@ -1790,10 +1797,13 @@ export function lowerObjectLiteral( // overridden. Field snapshots above preserve the copy order. let srcNode: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression; + const condRecord = conditionalRecordSpreadOf(lowerer, prop.expression); const srcLowered = capture( - prop === expr.properties.nodes[0] && leadingSpreadLowered !== null - ? leadingSpreadLowered - : lowerer.lowerExpr(srcNode), + condRecord !== null + ? lowerConditionalRecordSpread(lowerer, condRecord, locOf(prop)) + : prop === expr.properties.nodes[0] && leadingSpreadLowered !== null + ? leadingSpreadLowered + : lowerer.lowerExpr(srcNode), ); // Array callbacks can receive a record-or-undefined ABI even when // TypeScript describes a required record. Select the copy strategy @@ -2731,6 +2741,62 @@ function syntacticAndSpread( return { cond, props, whenTrue: true }; } +/** `...(c ? source : {})` (either arm order; the empty arm may also be + * `undefined` or `null`) where `source` maps to a record and is not the + * literal the field desugar handles (conditionalSpreadOf). The spread + * copies the source's fields when the source arm is taken and nothing + * otherwise, which is the copy of a `Record | undefined` spread source. */ +export function conditionalRecordSpreadOf( + lowerer: Lowerer, + expr: ts.Expression, +): { cond: ts.Expression; source: ts.Expression; whenTrue: boolean } | null { + let e: ts.Expression = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isConditionalExpression(e)) return null; + const unwrap = (a: ts.Expression): ts.Expression => { + let x = a; + while (ts.isParenthesizedExpression(x)) x = x.expression; + return x; + }; + const isEmpty = (x: ts.Expression): boolean => + (ts.isObjectLiteralExpression(x) && x.properties.nodes.length === 0) || + x.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(x) && + x.text === "undefined" && + (lowerer.typeOf(x).flags & ts.TypeFlags.Undefined) !== 0); + const whenTrue = unwrap(e.whenTrue); + const whenFalse = unwrap(e.whenFalse); + const sourceIsTrue = isEmpty(whenFalse) && !isEmpty(whenTrue); + if (!sourceIsTrue && !(isEmpty(whenTrue) && !isEmpty(whenFalse))) return null; + const source = sourceIsTrue ? whenTrue : whenFalse; + if (lowerer.mapTypeOf(lowerer.typeOf(source))?.kind !== "record") return null; + return { cond: e.condition, source, whenTrue: sourceIsTrue }; +} + +/** The spread source of a conditionalRecordSpreadOf match: `c ? source : + * undefined` as a `Record | undefined` value (cond first, the source only + * when its arm is taken). */ +function lowerConditionalRecordSpread( + lowerer: Lowerer, + spread: { cond: ts.Expression; source: ts.Expression; whenTrue: boolean }, + loc: SrcLoc, +): IrExpr { + const cond = lowerer.lowerCondition(spread.cond); + const source = lowerer.lowerExpr(spread.source); + if (source.type.kind !== "record") lowerer.badType(spread.source, lowerer.typeOf(spread.source)); + const type = lowerer.withUndefinedArm(source.type); + const present = lowerer.coerceToExpected(source, type); + const absent = lowerer.wrappedUndefined(type, loc)!; + return { + kind: "ternary", + cond, + then: spread.whenTrue ? present : absent, + else_: spread.whenTrue ? absent : present, + type, + loc, + }; +} + /** A conditional spread's carrier property: `name: value` or shorthand, * with an identifier/string-literal name. */ export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b5acaf9e6..6a80c551e 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,5 +1,6 @@ import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; +import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; /* Expression lowering: the expression dispatch (lowerExpr), literals @@ -575,6 +576,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // boundary for malformed/upstream ASTs. lowerer.unsupported("SC1090", expr, "top-level await (await outside async functions)"); } + const uniformAll = lowerAwaitedUniformPromiseAll(lowerer, expr); + if (uniformAll) return uniformAll; const value = lowerer.lowerExpr(expr.expression); // A promise-or-absent union (`Promise | undefined` values, calls // of `(...) => Promise | void` callbacks): the await handles diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..831fec83d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -67,9 +67,11 @@ import { F64, type IrExpr, type IrGlobal, + type IrLibFn, type IrLocal, type IrStmt, type IrType, + SEARCH_PARAMS_T, STRING, type SrcLoc, UNDEFINED_T, @@ -106,6 +108,7 @@ import { COMPOUND_ASSIGN_OPS, type CompoundOp, STR_METHODS, + NARROW_FIRST, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, @@ -3210,6 +3213,20 @@ export function lowerBindingPattern( lowerModuleNamespacePattern(lowerer, pattern, nsSource, isLet, out); return; } + if (srcType.kind === "url") { + lowerUrlObjectPattern(lowerer, pattern, srcRef, isLet, out); + return; + } + if ( + srcType.kind === "union" && + !isJsSourceFile(ts.getSourceFileOfNode(pattern)) && + lowerer.unions + .get(srcType.unionId) + ?.arms.every((arm) => arm.kind === "record" || arm.kind === "object") + ) { + lowerUnionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3424,7 +3441,10 @@ function patternSourceValue( !arms || !present?.length || present.length === arms.length || - (present.length > 1 && !(arrayPattern && present.every((arm) => arm.kind === "array"))) + (present.length > 1 && + !(arrayPattern + ? present.every((arm) => arm.kind === "array") + : present.every((arm) => arm.kind === "record" || arm.kind === "object"))) ) return value; for (const [tag, arm] of arms.entries()) { @@ -3541,6 +3561,153 @@ function lowerModuleNamespacePattern( } } +/** The URL getters an object pattern reads (`const { protocol, hostname } + * = new URL(text)`), each a read of the URL's current serialization. */ +const URL_PATTERN_GETTERS: Record = { + href: "url.href", + origin: "url.origin", + protocol: "url.protocol", + username: "url.username", + password: "url.password", + host: "url.host", + hostname: "url.hostname", + port: "url.port", + pathname: "url.pathname", + search: "url.search", + hash: "url.hash", +}; + +/** An object pattern over a URL: each element reads its getter in pattern + * order, as `url.name` does; `searchParams` binds the URL's live view. + * Methods and rest elements stay fenced. */ +function lowerUrlObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + isLet: boolean, + out: IrStmt[], +): void { + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) { + lowerer.unsupported( + "SC1031", + el, + "rest bindings over URL sources (a URL's properties are prototype getters, which rest elements do not copy)", + ); + } + const prop = el.propertyName ?? el.name; + const propName = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + const loc = locOf(el); + let value: IrExpr; + if (propName !== null && Object.hasOwn(URL_PATTERN_GETTERS, propName)) { + value = { + kind: "libCall", + fn: URL_PATTERN_GETTERS[propName]!, + args: [srcRef()], + type: STRING, + loc, + }; + } else if (propName === "searchParams") { + value = { + kind: "libCall", + fn: "url.searchParams", + args: [srcRef()], + type: SEARCH_PARAMS_T, + loc, + }; + } else { + lowerer.unsupported( + "SC1031", + el, + propName === null + ? "destructuring with computed keys that do not fold to one property name" + : `destructuring '${propName}' from a URL (${Object.keys(URL_PATTERN_GETTERS).join(", ")} and searchParams are the supported URL properties)`, + ); + } + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + +/** An object pattern over a union of records and class instances + * (`function f({ kind, path }: Add | Remove)`): each element reads a data + * field every arm declares with one type, through the runtime tag, as + * `value.field` does. Rest elements and fields the arms type differently + * stay fenced. */ +function lowerUnionObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + srcType: IrType & { kind: "union" }, + isLet: boolean, + out: IrStmt[], +): void { + const arms = lowerer.unions.get(srcType.unionId)?.arms ?? []; + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) { + lowerer.unsupported( + "SC1031", + el, + `rest bindings over '${lowerer.fmt(srcType)}' sources (${NARROW_FIRST})`, + ); + } + const prop = el.propertyName ?? el.name; + const propName = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + if (propName === null) { + lowerer.unsupported( + "SC1031", + el, + "destructuring with computed keys that do not fold to one property name", + ); + } + let common: IrType | null = null; + for (const arm of arms) { + let ft: IrType | undefined; + if (arm.kind === "record") { + ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type; + } else if (arm.kind === "object") { + ft = lowerer.classes.get(arm.className)?.fields.get(propName); + } + if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) { + common = null; + break; + } + common = ft; + } + if (common === null) { + lowerer.unsupported( + "SC1031", + el, + `destructuring '${propName}' from a '${lowerer.fmt(srcType)}' source (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`, + ); + } + let value: IrExpr = { + kind: "unionDisc", + unionId: srcType.unionId, + field: propName, + value: srcRef(), + type: common, + loc: locOf(el), + }; + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + /** Whether a pattern's first element has a computed key that does not * fold to one property name, so the key evaluates at run time. */ function startsWithRuntimeKey(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b02a0a843..d87ace914 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10784,9 +10784,29 @@ export class Lowerer { // Contextual types (such as Proxy descriptors' symbol-valued fields) // must not force an intermediate record with incompatible field slots. if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected); + if (this.returnsFreshCollection(node) && expected.kind !== "void") + return this.lowerExprExpecting(node, expected); return this.asyncReturnValueOf(node, this.lowerExpr(node), expected); } + /** An array literal or a `new Map(...)`, `new Set(...)` or `new + * Array(...)` of the builtins: a fresh collection, never a thenable, so + * an async return takes the declared result type as its construction + * layout the way a synchronous return does (`return []` in a function + * whose inferred result is `Promise`). */ + private returnsFreshCollection(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrayLiteralExpression(e)) return true; + return ( + ts.isNewExpression(e) && + ts.isIdentifier(e.expression) && + (this.isStdlibGlobal(e.expression, "Map") || + this.isStdlibGlobal(e.expression, "Set") || + this.isStdlibGlobal(e.expression, "Array")) + ); + } + /** A fresh JS object literal returned into a checked-native (dyn) result, * which lowers straight into that storage. */ private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean { @@ -10918,7 +10938,11 @@ export class Lowerer { loc, }; } - if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) { + if ( + this.adoptsReturnedPromise() && + !this.returnsLiteralIntoDyn(node, expected) && + !this.returnsFreshCollection(node) + ) { // The async return lowers its operand untyped first (a promise // adopts before its payload converts); a maybe-promise union needs // the statement form, which branches between adoption and fulfillment. diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index e3722bc32..126f80f93 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -11,6 +11,16 @@ export type WidthLift = | { how: "retag" } | { how: "unitRetag" } | { how: "liftWrap"; tag: number; arm: IrType } + /** A record into a union of two record arms that differ by one field: + * the source field `field` (`T | undefined`) selects `present` when it + * holds a value and `absent`, which lacks the field, when it is + * undefined. */ + | { + how: "presenceRoute"; + field: string; + present: { tag: number; arm: IrType }; + absent: { tag: number; arm: IrType }; + } | { how: "width" } | { how: "unionWidth" } | { how: "arr" } @@ -20,5 +30,6 @@ export type WidthLift = | { how: "clsWidth" } | { how: "narrow" } | { how: "dynIn" } + | { how: "dynOut" } | { how: "upcast" } | { how: "funcAdapt" }; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index bd2a32c37..aa275a27d 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2361,6 +2361,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const args = checker.getTypeArguments(ref); if (args.length === 0) return arrayOf(unitOnlyUnion(unions)); if (tupleShape?.elementFlags === undefined) return null; + // A leading-required tuple ending in a rest element whose positions + // all share one type (`[string, ...string[]]`, the non-empty list) + // is an array of that type: every read, write and method behaves as + // the array's, and tsc already proves the required positions exist. + const flags = tupleShape.elementFlags; + if ( + flags.length >= 2 && + (flags[flags.length - 1]! & ts.ElementFlags.Rest) !== 0 && + flags.slice(0, -1).every((f) => (f & ts.ElementFlags.Required) !== 0) + ) { + const elems = args.map((a) => mapType(a, ctx)); + const first = elems[0]; + if ( + first && + first.kind !== "void" && + isSupportedArrayElem(first) && + elems.every((e) => e !== null && typeEquals(e, first)) + ) + return arrayOf(first); + return null; + } // Optional/rest elements have no fixed native tuple layout. if (tupleShape.elementFlags.some((f) => !(f & ts.ElementFlags.Required))) { return null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json new file mode 100644 index 000000000..f6c5594a1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7020-conditional-spread-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7020-conditional-spread-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json new file mode 100644 index 000000000..c1e5f1ae4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7021-promise-all-uniform-tuple.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7021-promise-all-uniform-tuple.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json new file mode 100644 index 000000000..c1856ac5a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7022-promise-all-dynamic-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7022-promise-all-dynamic-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json new file mode 100644 index 000000000..bae130837 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7023-union-object-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7023-union-object-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json new file mode 100644 index 000000000..c4b9855ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7024-async-return-fresh-collections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7024-async-return-fresh-collections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json new file mode 100644 index 000000000..ed0e7b650 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7025-child-listener-chaining.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7025-child-listener-chaining.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json new file mode 100644 index 000000000..f050c4d1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7026-url-object-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7026-url-object-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json new file mode 100644 index 000000000..9b69861b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7027-search-params-conditional-init.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7027-search-params-conditional-init.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json new file mode 100644 index 000000000..a40ef1180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7028-non-empty-rest-tuples.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7028-non-empty-rest-tuples.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json new file mode 100644 index 000000000..72d564d3b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7029-flatmap-union-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7029-flatmap-union-results.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json new file mode 100644 index 000000000..57e76b621 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7033-any-fields-into-declared-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7033-any-fields-into-declared-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json new file mode 100644 index 000000000..a78b23cb1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7034-optional-field-union-routes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7034-optional-field-union-routes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json deleted file mode 100644 index 057d24d55..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts" - ], - "diags": [] -} diff --git a/tests/corpus/7020-conditional-spread-fields.ts b/tests/corpus/7020-conditional-spread-fields.ts new file mode 100644 index 000000000..61c250174 --- /dev/null +++ b/tests/corpus/7020-conditional-spread-fields.ts @@ -0,0 +1,137 @@ +// Conditional spreads that carry several properties, and conditional +// spreads of a record value against an empty arm. The condition runs +// once, the carried values run only when their arm is taken and in +// property order, and the keys appear at the spread's position. +const log: string[] = []; +function note(label: string, value: T): T { + log.push(label); + return value; +} + +interface Extra { + region: string; + zone: number; +} + +function build(flag: boolean, model: string | undefined, extra: Extra | undefined) { + return { + name: note("name", "tool"), + ...(note("cond", flag) ? { verbose: note("verbose", true), depth: note("depth", 2) } : {}), + ...(model ? { model } : {}), + ...(extra ? extra : {}), + last: note("last", 0), + }; +} + +for (const [flag, model, extra] of [ + [true, "m1", { region: "eu", zone: 3 }], + [false, undefined, undefined], + [true, undefined, { region: "us", zone: 1 }], +] as const) { + log.length = 0; + const out = build(flag, model, extra); + console.log(JSON.stringify(out), Object.keys(out).join(","), log.join(" ")); + console.log("verbose" in out, out.depth ?? "no depth", out.region ?? "no region"); +} + +// The carrying arm can be the false arm, and `c && { ... }` carries +// several properties too. +function flags(quiet: boolean, color: boolean) { + return { + ...(quiet ? {} : { level: "info", prefix: "[app]" }), + ...(color && { fg: "green", bg: "black", bold: true }), + }; +} +console.log(JSON.stringify(flags(false, true)), JSON.stringify(flags(true, false))); + +// A record arm in either position, against `{}`, `undefined` or `null`. +interface Auth { + user: string; + token?: string; +} +const auth: Auth = { user: "u", token: "t" }; +const bare: Auth = { user: "v" }; +function request(useAuth: boolean, source: Auth) { + return { + url: "/x", + ...(useAuth ? source : {}), + ...(useAuth ? undefined : { anonymous: true }), + }; +} +console.log(JSON.stringify(request(true, auth)), JSON.stringify(request(false, auth))); +console.log(JSON.stringify(request(true, bare)), Object.keys(request(true, bare)).join(",")); +function fallback(primary: Auth | null, secondary: Auth) { + return { ...(primary === null ? secondary : null), id: 1 }; +} +console.log(JSON.stringify(fallback(null, auth)), JSON.stringify(fallback(bare, auth))); + +// A record arm read through a call runs only when its arm is taken. +let reads = 0; +function current(): Extra { + reads++; + return { region: "ap", zone: reads }; +} +const a = { ...(reads > 0 ? current() : {}), tag: "a" }; +const b = { ...(reads === 0 ? current() : {}), tag: "b" }; +console.log(JSON.stringify(a), JSON.stringify(b), reads); + +// Into a declared shape with optional fields. +interface Options { + cwd: string; + shell?: boolean; + timeout?: number; + env?: string; +} +function options(detached: boolean, env: string | undefined): Options { + return { cwd: "/", ...(detached ? { shell: false, timeout: 5 } : {}), ...(env ? { env } : {}) }; +} +console.log(JSON.stringify(options(true, "prod")), JSON.stringify(options(false, undefined))); + +// Inside 'any'-typed object literals, where the key is absent when the +// empty arm is taken. +function anyLiterals(): void { + function opts(aspect: string | undefined): any { + const o: any = { + mode: "M", + ...(aspect ? { cfg: { aspect } } : {}), + }; + return o; + } + const a = opts("1:1"); + console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`); + const b = opts(undefined); + console.log(`${b.mode} ${typeof b.cfg}`); + + // Several carried properties. + function multi(on: boolean): any { + return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; + } + const m1 = multi(true); + const m2 = multi(false); + console.log(`${m1.base} ${m1.x} ${m1.y}`); + console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`); + + // Reversed orientation and string-literal keys. + function rev(quiet: boolean): any { + return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) }; + } + console.log(`${rev(false)["log-level"]}`); + console.log(`${typeof rev(true)["log-level"]}`); + + // The spread mid-literal: properties around it evaluate exactly once and + // land in engine key order. + let sides = 0; + function side(v: number): number { + sides++; + return v; + } + function ordered(flag: boolean): any { + const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) }; + return o; + } + const o1 = ordered(true); + const o2 = ordered(false); + console.log(`${o1.first} ${o1.mid} ${o1.last}`); + console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`); +} +anyLiterals(); diff --git a/tests/corpus/7021-promise-all-uniform-tuple.ts b/tests/corpus/7021-promise-all-uniform-tuple.ts new file mode 100644 index 000000000..461351992 --- /dev/null +++ b/tests/corpus/7021-promise-all-uniform-tuple.ts @@ -0,0 +1,46 @@ +// `const pair = await Promise.all([p, q])` over entries of one promise +// type: the checker types the binding as a tuple, and the values arrive +// on the same microtask turn as Node's, in entry order whatever the +// settlement order. +const order: string[] = []; +function later(label: string, turns: number): Promise { + return new Promise((resolve) => { + let left = turns; + const step = (): void => { + if (left-- === 0) { + order.push(`settle ${label}`); + resolve(`${label}!`); + } else queueMicrotask(step); + }; + step(); + }); +} +async function count(n: number): Promise { + return n * 10; +} + +async function main(): Promise { + let ticking = true; + void (async () => { + for (let i = 0; i < 20 && ticking; i++) { + order.push(`t${i}`); + await null; + } + })(); + const bodies = await Promise.all([later("a", 3), later("b", 1)]); + order.push(`bodies ${bodies[0]} ${bodies[1]} ${bodies.length}`); + const numbers = await Promise.all([count(1), count(2), count(3)]); + const [x, y, z] = numbers; + order.push(`numbers ${x + y + z} ${numbers.length}`); + const annotated: [string, string] = await Promise.all([later("c", 0), later("d", 0)]); + order.push(`annotated ${annotated.join("+")}`); + ticking = false; + console.log(order.join("\n")); + try { + const failed = await Promise.all([later("e", 2), Promise.reject(new Error("no"))]); + console.log("unreachable", failed[0]); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +void main(); diff --git a/tests/corpus/7022-promise-all-dynamic-arrays.ts b/tests/corpus/7022-promise-all-dynamic-arrays.ts new file mode 100644 index 000000000..dca5cdcde --- /dev/null +++ b/tests/corpus/7022-promise-all-dynamic-arrays.ts @@ -0,0 +1,41 @@ +// Promise.all over arrays built from 'any' values: the argument is a +// runtime value, so the combinator reads it with Node's iteration and +// resolves each entry (promises, thenables and plain values). +interface Check { + name: string; + ok: boolean; +} +async function check(name: string): Promise { + await null; + return { name, ok: name.length > 2 }; +} + +async function main(): Promise { + const raw: any = JSON.parse('{"items":[{"name":"abc"},{"name":"x"}],"ids":[1,2,3]}'); + const results = await Promise.all(raw.items.map((item: any) => check(item.name))); + for (const r of results) console.log(r.name, r.ok); + const doubled = await Promise.all(raw.ids.map(async (id: number) => id * 2)); + console.log(doubled.join(","), doubled.length); + const mixed = await Promise.all(raw.ids.map((id: number) => (id % 2 ? id : Promise.resolve(-id)))); + console.log(JSON.stringify(mixed)); + const parsed: unknown = JSON.parse('["a","bb","ccc"]'); + const list = Array.isArray(parsed) ? parsed : []; + const lengths = await Promise.all(list.map(async (s) => String(s).length)); + console.log(lengths, lengths.reduce((a, b) => a + b, 0)); + const empty: any = []; + console.log(JSON.stringify(await Promise.all(empty))); + try { + await Promise.all(raw.items.map(async (item: any) => { + if (item.name === "x") throw new Error(`bad ${item.name}`); + return item.name; + })); + } catch (e) { + console.log("rejected", (e as Error).message); + } + try { + await Promise.all(raw.missing); + } catch (e) { + console.log("not iterable", e instanceof TypeError); + } +} +void main(); diff --git a/tests/corpus/7023-union-object-patterns.ts b/tests/corpus/7023-union-object-patterns.ts new file mode 100644 index 000000000..8717e892a --- /dev/null +++ b/tests/corpus/7023-union-object-patterns.ts @@ -0,0 +1,63 @@ +// Object patterns over unions of records and classes read the fields +// every arm declares, in parameters, declarations and loops. +interface Add { + kind: "add"; + path: string; + content: string; + mode?: number; +} +interface Remove { + kind: "remove"; + path: string; + mode?: number; +} +type Change = Add | Remove; + +function describe({ kind, path: file, mode = 0o644 }: Change): string { + return `${kind}:${file}:${mode.toString(8)}`; +} +const changes: Change[] = [ + { kind: "add", path: "a.txt", content: "x" }, + { kind: "remove", path: "b.txt", mode: 0o600 }, +]; +for (const change of changes) console.log(describe(change)); +for (const { kind, path } of changes) console.log(kind, path.length); +function applyChange(change: Change): number { + const { path } = change; + let { kind } = change; + if (kind === "add") kind = "remove"; + return path.length + kind.length; +} +console.log(applyChange(changes[0]!), applyChange(changes[1]!)); + +// The source runs once, and the fields read in pattern order. +let reads = 0; +function next(): Change { + reads++; + return reads % 2 ? changes[0]! : changes[1]!; +} +const { path: p1, kind: k1 } = next(); +const { kind: k2 } = next(); +console.log(p1, k1, k2, reads); + +class Circle { + name: string; + size: number; + constructor(name: string, size: number) { + this.name = name; + this.size = size; + } +} +class Square { + name: string; + size: number; + corners = 4; + constructor(name: string, size: number) { + this.name = name; + this.size = size; + } +} +function label({ name, size }: Circle | Square): string { + return `${name}/${size}`; +} +console.log(label(new Circle("c", 2)), label(new Square("s", 3))); diff --git a/tests/corpus/7024-async-return-fresh-collections.ts b/tests/corpus/7024-async-return-fresh-collections.ts new file mode 100644 index 000000000..8343e028b --- /dev/null +++ b/tests/corpus/7024-async-return-fresh-collections.ts @@ -0,0 +1,48 @@ +// Async functions can return an empty array literal (where the result is +// inferred) or a new Map or Set: the value takes the result's element +// types, as a synchronous return does. +import { existsSync, readdirSync } from "node:fs"; + +async function listJson(dir: string) { + if (!existsSync(dir)) return []; + return readdirSync(dir).filter((name) => name.endsWith(".json")); +} + +async function childPaths(root: string) { + if (root === "") { + return []; + } + const found = [root, `${root}/b`]; + return found; +} + +async function index(names: string[]): Promise> { + if (names.length === 0) return new Map(); + const byName = new Map(); + for (const name of names) byName.set(name, name.length); + return byName; +} + +async function unique(words: string[]): Promise> { + if (words.length === 0) return new Set(); + return new Set(words); +} + +const pick = async (flag: boolean) => (flag ? [] : ["a", "b"]); + +async function main(): Promise { + const missing = await listJson("/nonexistent-dir-for-test"); + missing.push("pushed"); + console.log(missing, missing.length); + console.log(await childPaths(""), await childPaths("/root")); + const empty = await index([]); + empty.set("late", 4); + console.log(empty.size, empty.get("late"), (await index(["ab", "c"])).get("ab")); + const none = await unique([]); + none.add("z"); + console.log([...none], [...(await unique(["x", "x", "y"]))]); + const first = await pick(true); + first.push("c"); + console.log(first, await pick(false)); +} +void main(); diff --git a/tests/corpus/7025-child-listener-chaining.ts b/tests/corpus/7025-child-listener-chaining.ts new file mode 100644 index 000000000..317c87db1 --- /dev/null +++ b/tests/corpus/7025-child-listener-chaining.ts @@ -0,0 +1,42 @@ +// child.on() and child.once() return the child, so registrations chain +// and the result can be stored or passed on. The receiver evaluates once, +// before the listener. Each child starts after the previous one closed. +import { spawn } from "node:child_process"; + +const events: string[] = []; +let lookups = 0; +const children = [spawn("sh", ["-c", "exit 3"], { stdio: "ignore" })]; +function first() { + lookups++; + return children[0]!; +} + +const returned = first() + .on("exit", (code) => events.push(`exit ${code}`)) + .once("close", (code, signal) => { + events.push(`close ${code} ${signal}`); + console.log(events.join(", "), lookups); + second(); + }) + .on("error", (err) => events.push(`error ${err.message}`)); +console.log(returned === children[0], lookups); + +function second(): void { + spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }) + .on("spawn", () => console.log("spawned")) + .on("exit", (code) => console.log("second exit", code)) + .on("close", () => third()); +} + +function third(): void { + const missing = spawn("/nonexistent/command-for-test", [], { stdio: "ignore" }).on( + "error", + (e) => console.log("missing", (e as NodeJS.ErrnoException).code), + ); + console.log(typeof missing.pid); + const detached = spawn("sh", ["-c", "exit 0"], { detached: true, stdio: "ignore" }).once( + "error", + (e) => console.log("spawn failed", e.message), + ); + detached.unref(); +} diff --git a/tests/corpus/7026-url-object-patterns.ts b/tests/corpus/7026-url-object-patterns.ts new file mode 100644 index 000000000..c2bc59958 --- /dev/null +++ b/tests/corpus/7026-url-object-patterns.ts @@ -0,0 +1,27 @@ +// Object patterns over URLs read the URL's getters in pattern order, +// with renames and defaults, in declarations and parameters. +let built = 0; +function parse(text: string): URL { + built++; + return new URL(text); +} +const { protocol, hostname, port } = parse("http://localhost:8080/path?q=1#top"); +console.log(protocol, hostname, port, built); +const { host: where, pathname: path = "/", search, hash, href } = parse("https://example.com/a/b?x=1&y=2#h"); +console.log(where, path, search, hash, href, built); + +function origin({ protocol: scheme, host, username, password }: URL): string { + return `${scheme}//${username}:${password}@${host}`; +} +console.log(origin(new URL("ftp://user:secret@files.example.org:2121/dir"))); + +const { searchParams } = new URL("http://h/?a=1&b=2"); +searchParams.append("c", "3"); +console.log(searchParams.toString(), [...searchParams.keys()].join(",")); + +const { origin: o, port: p } = new URL("https://[::1]:8443/"); +console.log(o, p); +for (const text of ["http://a.test:1", "http://b.test"]) { + const { hostname: name, port: listen } = new URL(text); + console.log(name, listen === "" ? "(default)" : listen); +} diff --git a/tests/corpus/7027-search-params-conditional-init.ts b/tests/corpus/7027-search-params-conditional-init.ts new file mode 100644 index 000000000..580d6c039 --- /dev/null +++ b/tests/corpus/7027-search-params-conditional-init.ts @@ -0,0 +1,21 @@ +// new URLSearchParams(c ? a : b) builds from the arm the condition +// selects; each arm may be a record literal, a string, pairs or another +// URLSearchParams. +let checks = 0; +function flag(value: boolean): boolean { + checks++; + return value; +} +for (const on of [true, false]) { + const params = new URLSearchParams(flag(on) ? { code: "x y", state: "s&t" } : {}); + console.log(params.toString(), params.size); + const query = new URLSearchParams(on ? "a=1&b=2" : [["c", "3"]]); + console.log(query.toString()); + const base = new URLSearchParams("keep=1"); + const copy = new URLSearchParams(on ? base : undefined); + copy.append("more", "2"); + console.log(copy.toString(), base.toString()); + const nested = new URLSearchParams(on ? (checks > 1 ? { n: "2" } : { n: "1" }) : "?empty="); + console.log(nested.toString()); +} +console.log(checks); diff --git a/tests/corpus/7028-non-empty-rest-tuples.ts b/tests/corpus/7028-non-empty-rest-tuples.ts new file mode 100644 index 000000000..5d00952ab --- /dev/null +++ b/tests/corpus/7028-non-empty-rest-tuples.ts @@ -0,0 +1,38 @@ +// Tuples of required positions followed by a rest element of the same +// type (`[string, ...string[]]`, the non-empty list) behave as arrays of +// that type. +type Hosts = [string, ...string[]]; +const LOCAL_HOSTS: Hosts = ["127.0.0.1", "::1", "localhost"]; +const single: [string, ...string[]] = ["only"]; +const pair: readonly [number, number, ...number[]] = [1, 2]; + +function first(hosts: Hosts): string { + return hosts[0]; +} +function isLocalHost(host: string): boolean { + return LOCAL_HOSTS.includes(host); +} +console.log(first(LOCAL_HOSTS), first(single), LOCAL_HOSTS.length, single.length); +console.log(isLocalHost("::1"), isLocalHost("10.0.0.1"), LOCAL_HOSTS.indexOf("localhost")); +console.log(LOCAL_HOSTS.slice(1).join(","), LOCAL_HOSTS.map((h) => h.length)); +for (const host of LOCAL_HOSTS) console.log(host); +const [head, ...rest] = LOCAL_HOSTS; +console.log(head, rest, JSON.stringify(LOCAL_HOSTS)); +single.push("more"); +console.log(single, single[1], pair[0] + pair[1], pair.length); + +function hostsFrom(text: string): Hosts { + const parts = text.split(","); + const out: Hosts = [parts[0]!]; + for (const part of parts.slice(1)) out.push(part); + return out; +} +const parsed = hostsFrom("a,b,c"); +console.log(parsed, parsed.length, `${parsed}`); + +function join(...names: [string, ...string[]]): string { + return names.join("+"); +} +console.log(join("x"), join("x", "y", "z"), join(...LOCAL_HOSTS)); +const lists: Hosts[] = [LOCAL_HOSTS, single]; +console.log(lists.map((l) => l[0]).join(" ")); diff --git a/tests/corpus/7029-flatmap-union-results.ts b/tests/corpus/7029-flatmap-union-results.ts new file mode 100644 index 000000000..83c9583de --- /dev/null +++ b/tests/corpus/7029-flatmap-union-results.ts @@ -0,0 +1,31 @@ +// flatMap callbacks that return a value or an array: an array result +// appends its elements (holes skipped), any other result appends itself, +// and an empty `[]` arm appends nothing. +const words = ["a", "", "c", "", "e"]; +console.log(words.flatMap((s) => (s ? s : []))); +console.log(words.flatMap((s, i) => (s ? [s, String(i)] : []))); + +function expand(line: string): string | string[] { + return line.includes(",") ? line.split(",") : line; +} +console.log(["x", "y,z", "w"].flatMap(expand), ["x", "y,z"].flatMap((l) => expand(l)).length); + +const numbers = [1, 2, 3, 4]; +console.log(numbers.flatMap((n) => (n % 2 ? n : [n, n * 10]))); +const sparse: number[] = []; +sparse[0] = 1; +sparse[2] = 3; +console.log(numbers.flatMap((n) => (n === 2 ? sparse : n))); + +interface Entry { + name: string; + tags?: string[]; +} +const entries: Entry[] = [{ name: "a", tags: ["t1", "t2"] }, { name: "b" }]; +console.log(entries.flatMap((e) => e.tags ?? []), entries.flatMap((e) => e.tags ?? e.name)); +let calls = 0; +const out = words.flatMap((s) => { + calls++; + return s.length > 0 ? s.toUpperCase() : []; +}); +console.log(out, calls); diff --git a/tests/corpus/7033-any-fields-into-declared-records.ts b/tests/corpus/7033-any-fields-into-declared-records.ts new file mode 100644 index 000000000..89c0f69e8 --- /dev/null +++ b/tests/corpus/7033-any-fields-into-declared-records.ts @@ -0,0 +1,28 @@ +// Records whose fields TypeScript types `any` convert into declared +// record types: each such field is checked against the declared type, +// as a typed binding initialized from the value is. +interface Transfer { + source: string; + targetId: string; + bytes: number; + tags: string[]; +} +const meta: any = JSON.parse('{"id":"x-1","size":4,"tags":["a","b"]}'); + +const mapped: Transfer[] = ["a", "b"].map((s, i) => ({ + source: s, + targetId: meta.id, + bytes: meta.size + i, + tags: meta.tags, +})); +console.log(JSON.stringify(mapped)); + +function build(): Transfer { + const record = { source: "c", targetId: meta.id, bytes: meta.size, tags: [] as string[] }; + return record; +} +console.log(JSON.stringify(build())); + +const list: Transfer[] = []; +for (const entry of [{ source: "d", targetId: meta.id, bytes: 1, tags: meta.tags }]) list.push(entry); +console.log(list.length, list[0]!.targetId.toUpperCase(), list[0]!.tags.length); diff --git a/tests/corpus/7034-optional-field-union-routes.ts b/tests/corpus/7034-optional-field-union-routes.ts new file mode 100644 index 000000000..0fb93ff8d --- /dev/null +++ b/tests/corpus/7034-optional-field-union-routes.ts @@ -0,0 +1,26 @@ +// A record whose `T | undefined` field is the one field that tells two +// arms of a union apart converts into that union: a value picks the arm +// that declares the field, and undefined picks the arm without it. +type Summary = + | { written: number; errors: string[]; ignored: number } + | { written: number; ignored: number }; + +function summarize(written: number, ignored: number, errors: string[]): Summary { + return { written, errors: errors.length > 0 ? errors : undefined, ignored }; +} +function errorCount(summary: Summary): number { + return "errors" in summary && summary.errors !== undefined ? summary.errors.length : 0; +} +for (const errors of [[], ["e1", "e2"]]) { + const summary = summarize(3, 1, errors); + console.log(JSON.stringify(summary), errorCount(summary), summary.written + summary.ignored); +} + +type Lookup = { name: string; value: number; source: string } | { name: string; source: string }; +const values = new Map([["a", 1]]); +const lookups: Lookup[] = ["a", "b"].map((name) => ({ + name, + value: values.get(name), + source: "map", +})); +console.log(JSON.stringify(lookups)); diff --git a/tests/diagnostics/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index e42f8a07a..0ee60579d 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -38,12 +38,12 @@ interface E1 { } interface E2 { kind: "two"; - b: string; + b: number; label: string; } function fromUnion(u: E1 | E2): string { - const { b } = u; // union source: narrow first - return b; + const { b } = u; // union source whose arms type the field differently: narrow first + return String(b); } console.log(fromUnion({ kind: "one", b: "s", count: 1 })); diff --git a/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts b/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts deleted file mode 100644 index 03043a32f..000000000 --- a/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts +++ /dev/null @@ -1,49 +0,0 @@ -// The conditional-spread idiom inside `any`-typed object literals: -// `{ a, ...(c ? { k: v } : {}) }` builds one of two engine literals behind -// the test, so the key is truly ABSENT when the empty arm is taken — -// engine-exact (`typeof o.k` answers "undefined", JSON drops it), with the -// shared properties evaluating exactly once. - -function opts(aspect: string | undefined): any { - const o: any = { - mode: "M", - ...(aspect ? { cfg: { aspect } } : {}), - }; - return o; -} -const a = opts("1:1"); -console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`); -const b = opts(undefined); -console.log(`${b.mode} ${typeof b.cfg}`); - -// Multi-property carrier arms are fine in the island form. -function multi(on: boolean): any { - return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; -} -const m1 = multi(true); -const m2 = multi(false); -console.log(`${m1.base} ${m1.x} ${m1.y}`); -console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`); - -// Reversed orientation and string-literal keys. -function rev(quiet: boolean): any { - return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) }; -} -console.log(`${rev(false)["log-level"]}`); -console.log(`${typeof rev(true)["log-level"]}`); - -// The spread mid-literal: properties around it evaluate exactly once and -// land in engine key order. -let sides = 0; -function side(v: number): number { - sides++; - return v; -} -function ordered(flag: boolean): any { - const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) }; - return o; -} -const o1 = ordered(true); -const o2 = ordered(false); -console.log(`${o1.first} ${o1.mid} ${o1.last}`); -console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`); diff --git a/tests/harness/__snapshots__/child-process.ts.txt b/tests/harness/__snapshots__/child-process.ts.txt index f5f9f8f86..a1f2999b7 100644 --- a/tests/harness/__snapshots__/child-process.ts.txt +++ b/tests/harness/__snapshots__/child-process.ts.txt @@ -14,7 +14,7 @@ child-process.ts:27:14 - error SC1090: listeners returning a value (make the cal | ^~~~~~~~~~~~~~~ 28 | // Methods have no bound-value form — call on directly. -child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): void; (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "close", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; (event: "message", listener: (message:...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature +child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): ChildProcess; (event: "exit", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "close", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "error", listener: (err: Error) => void): ChildProcess; (event...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature 28 | // Methods have no bound-value form — call on directly. 29 | const f = c.on; diff --git a/tests/harness/__snapshots__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 743bb5657..2f6afe734 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -19,18 +19,18 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a | ^~~~~~ 32 | console.log(onlyIn); -destructuring-fences.ts:45:9 - error SC1031: object destructuring of non-record values (the source is union-typed) are not supported yet +destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a '{ b: string; count: number; kind: string } | { b: number; kind: string; label: string }' source (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet 44 | function fromUnion(u: E1 | E2): string { - 45 | const { b } = u; // union source: narrow first - | ^~~~~ - 46 | return b; + 45 | const { b } = u; // union source whose arms type the field differently: narrow first + | ^ + 46 | return String(b); -destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on its declaration +destructuring-fences.ts:46:17 - error SC2004: uses of 'b' inherit the blocker on its declaration - 45 | const { b } = u; // union source: narrow first - 46 | return b; - | ^ + 45 | const { b } = u; // union source whose arms type the field differently: narrow first + 46 | return String(b); + | ^ 47 | } hint: the declaration of 'b' did not compile — fix the diagnostic reported there and these sites clear with it diff --git a/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt b/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt deleted file mode 100644 index 707d1dfd9..000000000 --- a/tests/harness/__snapshots__/retired-engine-1123-any-conditional-spread.ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -retired-engine-1123-any-conditional-spread.ts:21:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet - - 20 | function multi(on: boolean): any { - 21 | return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) }; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 22 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/spread-fences.ts.txt b/tests/harness/__snapshots__/spread-fences.ts.txt index 56f8b81ee..eaced4979 100644 --- a/tests/harness/__snapshots__/spread-fences.ts.txt +++ b/tests/harness/__snapshots__/spread-fences.ts.txt @@ -1,4 +1,4 @@ -spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet +spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields) is not supported yet 13 | const cond = true; 14 | const s1: Shape = { ...(cond ? { x: 1, y: 2 } : { x: 3, y: 4 }) }; diff --git a/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json b/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json new file mode 100644 index 000000000..ee564e79f --- /dev/null +++ b/tests/harness/corpus-costs/7020-conditional-spread-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 3640, "san": 5580 } diff --git a/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json b/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json new file mode 100644 index 000000000..fba6fd530 --- /dev/null +++ b/tests/harness/corpus-costs/7021-promise-all-uniform-tuple.ts.json @@ -0,0 +1 @@ +{ "plain": 2510, "san": 3630 } diff --git a/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json b/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json new file mode 100644 index 000000000..c59bb483b --- /dev/null +++ b/tests/harness/corpus-costs/7022-promise-all-dynamic-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 3330, "san": 4690 } diff --git a/tests/harness/corpus-costs/7023-union-object-patterns.ts.json b/tests/harness/corpus-costs/7023-union-object-patterns.ts.json new file mode 100644 index 000000000..3f0a047e7 --- /dev/null +++ b/tests/harness/corpus-costs/7023-union-object-patterns.ts.json @@ -0,0 +1 @@ +{ "plain": 2440, "san": 3670 } diff --git a/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json b/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json new file mode 100644 index 000000000..6285a25c5 --- /dev/null +++ b/tests/harness/corpus-costs/7024-async-return-fresh-collections.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4360 } diff --git a/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json b/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json new file mode 100644 index 000000000..44843dc06 --- /dev/null +++ b/tests/harness/corpus-costs/7025-child-listener-chaining.ts.json @@ -0,0 +1 @@ +{ "plain": 2510, "san": 3950 } diff --git a/tests/harness/corpus-costs/7026-url-object-patterns.ts.json b/tests/harness/corpus-costs/7026-url-object-patterns.ts.json new file mode 100644 index 000000000..9d3f28ab1 --- /dev/null +++ b/tests/harness/corpus-costs/7026-url-object-patterns.ts.json @@ -0,0 +1 @@ +{ "plain": 2520, "san": 4000 } diff --git a/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json b/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json new file mode 100644 index 000000000..56c46a74b --- /dev/null +++ b/tests/harness/corpus-costs/7027-search-params-conditional-init.ts.json @@ -0,0 +1 @@ +{ "plain": 2240, "san": 3440 } diff --git a/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json b/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json new file mode 100644 index 000000000..147f70b9a --- /dev/null +++ b/tests/harness/corpus-costs/7028-non-empty-rest-tuples.ts.json @@ -0,0 +1 @@ +{ "plain": 2520, "san": 4030 } diff --git a/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json b/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json new file mode 100644 index 000000000..7efdd91bf --- /dev/null +++ b/tests/harness/corpus-costs/7029-flatmap-union-results.ts.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 4710 } diff --git a/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json b/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json new file mode 100644 index 000000000..1fd3ee053 --- /dev/null +++ b/tests/harness/corpus-costs/7033-any-fields-into-declared-records.ts.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 3950 } diff --git a/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json new file mode 100644 index 000000000..7e342cfd0 --- /dev/null +++ b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json @@ -0,0 +1 @@ +{ "plain": 2420, "san": 3640 } From 14694925c7d2edfa5c681c2d623f39b0ce3494af Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:32:24 -0500 Subject: [PATCH 016/169] Bind a class expression's name used only in a shorthand property, and stop keeping local class prototypes alive - A named function-local class expression whose only reference to its own name is a shorthand property (`static wrap() { return { Packed }; }`) now gets its name binding. The self-reference check read the shorthand name's property symbol, so the class lowered without the binding and the property held a placeholder function instead of the class. - A function-local class's prototype takes its name from the class object's interned name instead of a registered copy. The registry kept every such prototype alive until the process exited, so evaluating a local class repeatedly kept each evaluation's prototype and the method state it captures: a program that evaluates a local class 200,000 times and prints each instance peaked at 121 MB, and now at 2 MB. Reusing a freed object clears its name. --- .../compiler/src/frontend/lowering/lower-classes.ts | 12 +++++------- packages/runtime/src/scr_dyn.h | 12 +++++++----- packages/runtime/src/scr_json.c | 13 ++++++------- .../6917-local-class-expression-self-reference.ts | 12 ++++++++++++ .../6918-local-class-expression-self-reference.mjs | 11 +++++++++++ 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index c74969d10..f16d5ee5d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -184,6 +184,7 @@ import { constructionEscape } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; import { reflectedClassStaticMethodValue } from "./class-method-values.js"; +import { identifierReadSymbol } from "./surfaces.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { const body = member.body; @@ -6035,8 +6036,9 @@ export function lowerClassExpressionInfo( } /** True when a named class expression's body refers to the class value by - * its own name (`class Box { static of() { return new Box(); } }`); type - * annotations naming it (`again(): Box`) do not count. */ + * its own name (`class Box { static of() { return new Box(); } }`, a + * shorthand property `{ Box }` included); type annotations naming it + * (`again(): Box`) do not count. */ function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): boolean { const name = expr.name; if (!name) return false; @@ -6046,11 +6048,7 @@ function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): for (const member of expr.members.nodes) { ts.walkPreorder(member, (n) => { if (found || ts.isTypeNode(n)) return "skip"; - if ( - ts.isIdentifier(n) && - n.text === name.text && - lowerer.checker.getSymbolAtLocation(n) === symbol - ) + if (ts.isIdentifier(n) && n.text === name.text && identifierReadSymbol(lowerer, n) === symbol) found = true; return undefined; }); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 8a29e7c5d..7c62aa05e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -370,11 +370,13 @@ struct ScrDyn { void *source_identity; ScrDyn *(*source_access)(void *, bool materialize); struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ - /* On a runtime-built class prototype (e.g. url.Url.prototype): the - * constructor name (static text) util.inspect prints for objects - * inheriting from it (scr_dyn_class_name). NULL otherwise. Only - * prototypes that live until their thread exits set it, and they - * clear it before release, so freelist reuse never sees it. */ + /* On a runtime-built class prototype (e.g. url.Url.prototype) and + * on a compiled class's prototype object: the constructor name + * (static or immortal text) util.inspect prints for objects + * inheriting from it (scr_dyn_class_name). NULL otherwise. Names + * that are copies live on prototypes that live until their thread + * exits, and those clear it before release; reuse of a freed object + * resets it (scr_dyn_alloc). */ const char *class_name; struct ScrDynRegion *region; /* see ScrDynRegion */ /* The dynamic tier's hidden class (scr_js_object.c): the transition diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 6986330f5..007f8a766 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1021,6 +1021,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { } else if (kind == SCR_DYN_OBJ) { d->v.obj.len = 0; /* cap/entries preserved */ d->v.obj.region = NULL; + d->v.obj.class_name = NULL; /* a released class prototype's name */ SCR_JS_SHAPE_RESET(d); if (d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS) { for (size_t i = 0; i < d->v.obj.cap; i++) { @@ -4185,13 +4186,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { if (!cls->prototype_data) { ScrDyn *data = base->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : scr_dyn_obj_create(base); if (!data) return NULL; - /* util.inspect prints an instance with its class's name. */ - const char *name = value->v.fn.name; - if (name && name[0]) { - ScrStr *class_name = scr_str_new(name, strlen(name)); - scr_dyn_name_class_prototype(data, class_name); - scr_str_release(class_name); - } + /* util.inspect prints an instance with its class's name: the class + * object's interned name, which is immortal. The prototype is owned by + * the class object and no registry keeps it alive, so evaluating a + * function-local class again releases the previous one's prototype. */ + if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } diff --git a/tests/corpus/6917-local-class-expression-self-reference.ts b/tests/corpus/6917-local-class-expression-self-reference.ts index f11746eda..cff612d29 100644 --- a/tests/corpus/6917-local-class-expression-self-reference.ts +++ b/tests/corpus/6917-local-class-expression-self-reference.ts @@ -39,3 +39,15 @@ function nested(n: number) { return Outer.build(n).size(); } console.log(nested(3)); + +// The only reference to the name is a shorthand property. +function registry(tag: string) { + return class Entry { + readonly tag = tag; + static table(): Record { + return { Entry }; + } + }; +} +const Entry1 = registry("e"); +console.log(Entry1.table().Entry === Entry1, new Entry1().tag); diff --git a/tests/corpus/6918-local-class-expression-self-reference.mjs b/tests/corpus/6918-local-class-expression-self-reference.mjs index cfc8de806..4b2440674 100644 --- a/tests/corpus/6918-local-class-expression-self-reference.mjs +++ b/tests/corpus/6918-local-class-expression-self-reference.mjs @@ -48,3 +48,14 @@ function factory(tag) { const T = factory("t"); T.make(); console.log(T.make().tag, T.count, factory("u").count); + +// The only reference to the name is a shorthand property. +function packed() { + const K = class Packed { + static wrap() { + return { Packed }; + } + }; + return [K.wrap().Packed === K, K.wrap().Packed.name]; +} +console.log(packed()); From 01939bc0721d00fe519fc23a66026a7f96193ab8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:41:08 -0500 Subject: [PATCH 017/169] Give arrays state bytes only once they hold a hole or undefined An array starts without per-slot state bytes. While it has none, every index below its length is a present value in dense storage (len <= cap, no sparse entries) and its hole-free prefix bound `filled` equals its length, so its slots take 8 bytes per element (1 for booleans) instead of 9 (2). The first hole or present undefined below the length, a length set past the elements, or a sparse entry gives it states (scr_arr_add_states), which it keeps; SCR_ARR_STATES in `spare` records it, and the element ops id moves up one bit. Runtime readers take scr_arr_dense_state or scr_arr_scan_states, and writers keep the elements contiguous from 0 or give the array states before raising its length. A length set on an empty array with inline slots reserves its slots and states in one allocation. Emitted reads keep their shape: they load a state only at or past `filled`, which an array without states never reaches. A store past `filled` tests the flag and, without states, appends at the length; push and pop write a state byte only when the array has states. map, fused map and slice pipelines that keep holes, and concat's element copy reserve the result's storage, fill it, and set its length afterwards, so a result without holes is filled by appends and gets no states. The typed-view cache refresh moves the flag with the storage it swaps, and tagged-template arrays carry no state bytes. --- packages/compiler/src/backend/llvm/common.ts | 11 +- .../backend/llvm/dense-array-access.test.ts | 49 ++ .../src/backend/llvm/dense-array-access.ts | 85 ++- packages/compiler/src/backend/llvm/dyn.ts | 28 +- .../compiler/src/backend/llvm/emit-module.ts | 16 +- .../src/backend/llvm/emit-statements.ts | 5 + .../src/backend/llvm/expr-primitives.test.ts | 15 +- .../lowering/containers/array-callbacks.ts | 15 + .../lowering/containers/array-concat.ts | 10 + .../lowering/containers/array-pipelines.ts | 27 +- packages/compiler/src/ir/ir.ts | 14 +- packages/runtime/src/scr_array.c | 531 +++++++++++++----- packages/runtime/src/scr_array.h | 77 ++- packages/runtime/src/scr_bytes.c | 2 +- packages/runtime/src/scr_copying.c | 28 +- packages/runtime/src/scr_cycle.c | 4 +- packages/runtime/src/scr_itable.c | 2 +- packages/runtime/src/scr_itable.test.c | 2 + packages/runtime/src/scr_lib.c | 24 +- packages/runtime/src/scr_map.c | 2 +- packages/runtime/src/scr_publish.c | 2 +- packages/runtime/src/scr_regex.c | 24 +- packages/runtime/src/scr_region.c | 2 +- packages/runtime/src/scr_string.c | 12 +- packages/runtime/test/test_array.c | 191 ++++++- 25 files changed, 903 insertions(+), 275 deletions(-) diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index 822ee29e2..ac4e28708 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -10,9 +10,10 @@ import type { import type { LlvmEmitterContext } from "./expr-context.js"; /** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for - * field (its static assertions pin `len`, `cap`, `cyc`, `filled` and - * `data`). `len` and `cap` are i32 on every target; the dense states end - * the allocation after the `cap` value slots at `data` (scrArrStates). */ + * field (its static assertions pin `len`, `cap`, `cyc`, `spare`, `filled` + * and `data`). `len` and `cap` are i32 on every target; the dense states of + * an array with states (SCR_ARR_STATES in `spare`) end the allocation after + * the `cap` value slots at `data` (scrArrStates). */ export const SCR_ARR_FIELD = { rc: 0, len: 1, @@ -24,6 +25,10 @@ export const SCR_ARR_FIELD = { data: 7, } as const; +/** The `spare` flag of a %ScrArr whose dense slots carry state bytes + * (SCR_ARR_STATES in scr_array.h). */ +export const SCR_ARR_STATES = 16; + /** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a * size-type count. */ export function loadArrCount( diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index 1b84cf0a0..6a0123559 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -316,6 +316,55 @@ test.each([32, 64] as const)( }, ); +test("arrays without state bytes skip the state stores", () => { + const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc }; + const write: IrStmt = { + kind: "arraySet", + arr: ref("s", words), + index, + value: { kind: "strLit", value: "x", type: STRING, loc }, + loc, + }; + const push: IrStmt = { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", nums), + args: [{ kind: "numLit", value: 2.5, type: F64, loc }], + type: F64, + loc, + }, + loc, + }; + const llvm = emitLlvmModule( + module("plain", STRING, [push, write, { kind: "return", value: read, loc }]), + ); + const fn = body(llvm, "plain"); + // SCR_ARR_STATES (16) in `spare`, field 5. + const flag = String.raw`getelementptr inbounds %ScrArr, ptr %t\d+, i32 0, i32 5\n %t\d+ = load i16, ptr %t\d+, !tbaa !\d+\n %t\d+ = and i16 %t\d+, 16\n %t\d+ = icmp ne i16 %t\d+, 0\n`; + // A push marks the state only when the array has states. + expect(fn).toMatch( + new RegExp( + `${flag} br i1 %t\\d+, label %arr\\.push\\.state\\d+, label %arr\\.push\\.marked\\d+\\narr\\.push\\.state\\d+:\\n store i8 1, ptr `, + ), + ); + // Above the prefix, an array without states has no elements (its prefix + // is its length): a store there appends at the length and replaces no + // value. + expect(fn).toMatch( + new RegExp( + `${flag} br i1 %t\\d+, label %arr\\.set\\.states\\d+, label %arr\\.set\\.plain\\d+`, + ), + ); + expect(fn).toMatch( + /^arr\.set\.plain\d+:\n %t\d+ = icmp eq i64 .*\n br i1 %t\d+, label %arr\.set\.values\d+, label %arr\.set\.slow\d+$/m, + ); + expect(fn).toMatch(/phi i1 \[ %t\d+, %arr\.set\.states\d+ \], \[ false, %arr\.set\.values\d+ \]/); + // Reads test no flag: they reach the state check only above the prefix. + expect(fn).not.toMatch(/label %arr\.get\.state\d+/); +}); + test("state and presence queries answer dense slots inline", () => { const state: IrExpr = { kind: "arrayState", arr: ref("b", flags), index, type: F64, loc }; const has: IrExpr = { kind: "arrayHas", arr: ref("s", words), index, type: BOOL, loc }; diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 4488f90cb..4d8d5fecf 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -14,13 +14,17 @@ * state bytes altogether: the runtime keeps every state below it a present * value, so reads load the slot directly and writes replace it without * touching its state. Other dense indices take the state-checked path, - * whose writes extend the bound when they continue it. */ + * whose writes extend the bound when they continue it. An array without + * state bytes (SCR_ARR_STATES clear in `spare`) keeps its bound at its + * length, so reads never reach the state check for it; a write past the + * bound tests the flag and, without states, appends at the length. */ import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; import { f64Lit, loadArrCount, SCR_ARR_FIELD, + SCR_ARR_STATES, scrArrStatePtr, scrArrStates, sizeToDouble, @@ -31,6 +35,24 @@ import { exactInteger, widenInteger } from "./integer-values.js"; const SCR_ARR_VALUE = 1; +/** Whether the array at `array` has state bytes, as an i1. `spare` changes + * only in runtime calls, so its loads take their own alias tag. */ +export function emitArrayHasStates(host: LlvmEmitterContext, array: string): string { + const B = host.B; + const sparePtr = B.tmp(), + spare = B.tmp(), + bit = B.tmp(), + has = B.tmp(); + B.line( + `${sparePtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.spare}`, + ); + host.markMemoryPointer(sparePtr, "array:spare"); + B.line(`${spare} = load i16, ptr ${sparePtr}${host.fieldAliasAttachment(sparePtr)}`); + B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_STATES}`); + B.line(`${has} = icmp ne i16 ${bit}, 0`); + return has; +} + /** Call-site effects of the non-retaining read fallbacks: they only read * module-visible memory, or trap without returning to the caller (the trap * writes runtime-private stderr/TLS state). Declaring this lets LLVM keep @@ -482,18 +504,48 @@ export function emitDenseArraySet( B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } }; + const stated = B.newLabel("arr.set.states"), + plain = B.newLabel("arr.set.plain"), + plainStore = B.newLabel("arr.set.values"), + stored = B.newLabel("arr.set.stored.slot"); + B.condBr(emitArrayHasStates(host, array), stated, plain); + B.startBlock(stated); const statePtr = statePointer(host, guard); const valuePtr = valuePointer(host, guard, acc); - let oldState: string | null = null; - let old: string | null = null; + let statedOld: string | null = null; + let statedHad: string | null = null; if (acc === "ref") { - oldState = B.tmp(); - old = B.tmp(); + const oldState = B.tmp(); + statedOld = B.tmp(); + statedHad = B.tmp(); B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); - B.line(`${old} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`${statedOld} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + B.line(`${statedHad} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); } storeValue(valuePtr); B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + const statedEnd = B.currentLabel(); + B.br(stored); + // Without states the bound is the length, so a store here appends at + // the length (it replaces no value) or leaves holes, which the runtime + // gives states. + B.startBlock(plain); + const appends = B.tmp(); + B.line(`${appends} = icmp eq ${host.sizeType} ${offset}, ${len}`); + B.condBr(appends, plainStore, slow); + B.startBlock(plainStore); + storeValue(valuePointer(host, guard, acc)); + const plainEnd = B.currentLabel(); + B.br(stored); + B.startBlock(stored); + let old: string | null = null; + let hadValue: string | null = null; + if (acc === "ref") { + old = B.tmp(); + hadValue = B.tmp(); + B.line(`${old} = phi ptr [ ${statedOld}, %${statedEnd} ], [ null, %${plainEnd} ]`); + B.line(`${hadValue} = phi i1 [ ${statedHad}, %${statedEnd} ], [ false, %${plainEnd} ]`); + } // A store at the bound continues the hole-free prefix. const filledWide = sizedFilled(host, guard.filled), continues = B.tmp(); @@ -537,9 +589,7 @@ export function emitDenseArraySet( storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr)); B.br(storedLabel); B.startBlock(storedLabel); - if (acc === "ref" && oldState && old) { - const hadValue = B.tmp(); - B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); + if (acc === "ref" && hadValue && old) { const releaseLabel = B.newLabel("arr.set.release"); B.condBr(hadValue, releaseLabel, join); B.startBlock(releaseLabel); @@ -627,7 +677,14 @@ export function emitDenseArrayPop( raw = wide; } B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + // Only an array with states has a state byte to clear. + const clear = B.newLabel("arr.pop.state"), + cleared = B.newLabel("arr.pop.cleared"); + B.condBr(emitArrayHasStates(host, array), clear, cleared); + B.startBlock(clear); B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + B.br(cleared); + B.startBlock(cleared); let last32 = last; if (size === "i64") { last32 = B.tmp(); @@ -698,7 +755,14 @@ export function emitDenseArrayPush( } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } + // Only an array with states has a state byte to mark. + const mark = B.newLabel("arr.push.state"), + marked = B.newLabel("arr.push.marked"); + B.condBr(emitArrayHasStates(host, array), mark, marked); + B.startBlock(mark); B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); + B.br(marked); + B.startBlock(marked); const next = B.tmp(), fastValue = B.tmp(); B.line(`${next} = add nuw ${size} ${len}, 1`); @@ -716,6 +780,7 @@ export function emitDenseArrayPush( B.line(`${bumped} = select i1 ${packed}, i32 ${nextFilled}, i32 ${filled}`); B.line(`store i32 ${bumped}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); B.line(`${fastValue} = ${sizeToDouble(size)} ${size} ${next} to double`); + const fastEnd = B.currentLabel(); B.br(join); B.startBlock(slow); const slowValue = B.tmp(); @@ -723,6 +788,6 @@ export function emitDenseArrayPush( B.br(join); B.startBlock(join); const result = B.tmp(); - B.line(`${result} = phi double [ ${fastValue}, %${fast} ], [ ${slowValue}, %${slow} ]`); + B.line(`${result} = phi double [ ${fastValue}, %${fastEnd} ], [ ${slowValue}, %${slow} ]`); return result; } diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6911ef98b..86ea79907 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,6 +1,6 @@ import { typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; -import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit, sizeToDouble } from "./common.js"; +import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { INLINE_UNION_FIELD, @@ -1708,6 +1708,32 @@ export class LlDyn { B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`); }); if (t.kind !== "record") { + // Whether the dense slots carry states (SCR_ARR_STATES in + // `spare`) moves with them; the rest of `spare` stays. + const cachedSpare = B.tmp(), + checkedSpare = B.tmp(); + const sparePtr = (array: string): string => { + const ptr = B.tmp(); + B.line( + `${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i64 0, i32 ${SCR_ARR_FIELD.spare}`, + ); + return ptr; + }; + const cachedSparePtr = sparePtr(cached), + checkedSparePtr = sparePtr(checked); + B.line(`${cachedSpare} = load i16, ptr ${cachedSparePtr}`); + B.line(`${checkedSpare} = load i16, ptr ${checkedSparePtr}`); + const exchange = (own: string, other: string, ptr: string): void => { + const kept = B.tmp(), + moved = B.tmp(), + next = B.tmp(); + B.line(`${kept} = and i16 ${own}, ${~SCR_ARR_STATES}`); + B.line(`${moved} = and i16 ${other}, ${SCR_ARR_STATES}`); + B.line(`${next} = or i16 ${kept}, ${moved}`); + B.line(`store i16 ${next}, ptr ${ptr}`); + }; + exchange(cachedSpare, checkedSpare, cachedSparePtr); + exchange(checkedSpare, cachedSpare, checkedSparePtr); // Sparse slots and numeric properties live out of line; each // array keeps its own match metadata. host.declare("declare void @scr_arr_swap_indexed_ext(ptr, ptr)"); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..bf89784b7 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -699,21 +699,19 @@ export function emitModuleParts(emitter: LlEmitter): string[] { for (const code of regexMatchers.values()) out.push(...code.lines, ``); for (const [, inst] of emitter.templateStringsInstances) { // One immortal ScrArr per tagged-template site: a data global of - // interned cooked-string literals followed by their states (the - // runtime finds them before data + cap * 9), and the ScrArr header over - // it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF - // entry points). Every dense slot is present; reads retain immortal - // strings — a no-op. Slots are 8 bytes on every target: on 32-bit - // targets each holds the pointer in its low half and zero in its high - // half, so the states start where the runtime looks for them. + // interned cooked-string literals, and the ScrArr header over it + // (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF entry + // points, no state bytes: every dense slot is present). Reads retain + // immortal strings — a no-op. Slots are 8 bytes on every target: on + // 32-bit targets each holds the pointer in its low half and zero in its + // high half. const n = inst.slots.length; - const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`; const wide = emitter.sizeType === "i64"; const slot = wide ? "ptr" : "<{ ptr, i32 }>"; const slotValue = (s: string): string => wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`; out.push( - `@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`, + `@${inst.sym}_data = internal constant [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], align 8`, `@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr @${inst.sym}_data }`, ); } diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7239db7d8..5376d4f53 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -435,6 +435,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const length = emitter.emitExpr(s.length); if (s.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array"); + if (s.reserve === true) { + emitter.declare(`declare void @scr_arr_reserve_len(ptr, double)`); + B.line(`call void @scr_arr_reserve_len(ptr ${arr.name}, double ${length.name})`); + break; + } emitter.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3); emitter.declare(`declare void @scr_arr_set_len(ptr, double)`); // Assigning the current length (a reused work array reset to where diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 41ecd189e..726ec20c5 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -308,7 +308,7 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits" }); test.each([32, 64] as const)( - "tagged-template string arrays keep 8-byte slots before their states on %i-bit targets", + "tagged-template string arrays keep 8-byte slots without states on %i-bit targets", (bits) => { const strings: IrExpr = { kind: "templateStrings", @@ -321,14 +321,17 @@ test.each([32, 64] as const)( mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }]; const llvm = emitLlvmModule(mod, { pointerBits: bits }); const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0]; - // The runtime reads each slot as 8 bytes and finds the states after - // cap * 8 bytes of slots (scr_arr_states_end). - if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>"); + // The runtime reads each slot as 8 bytes; an array without states + // (`spare` 0 in its header) has no state bytes after them. + if (bits === 64) expect(data).toMatch(/^\S+ = internal constant \[3 x ptr\] /); else { - expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>"); + expect(data).toMatch(/^\S+ = internal constant \[3 x <\{ ptr, i32 \}>\] /); expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3); } - expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]"); + expect(data).not.toContain("x i8]"); + expect(llvm).toMatch( + /^@sc_tsa_0 = internal global %ScrArr \{ i\d+ -1, i32 3, i32 3, i8 2, i8 0, i16 0, i32 3, /m, + ); }, ); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 612c47ea0..cc22594d5 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -778,6 +778,10 @@ function buildArrayHofFn( loc, }, readLen, + // map's result has the receiver's length, holes where the receiver + // has them. Storage for it is reserved first and the length is set + // after the loop, so a result without holes is filled by appends and + // gets no state bytes. ...(method === "map" ? [ { @@ -785,6 +789,7 @@ function buildArrayHofFn( arr: varRef("out.0", outT, loc), length: varRef("n.0", F64, loc), loc, + reserve: true, } satisfies IrStmt, ] : []), @@ -816,6 +821,16 @@ function buildArrayHofFn( loc, }, ]), + ...(method === "map" + ? [ + { + kind: "arraySetLength", + arr: varRef("out.0", outT, loc), + length: varRef("n.0", F64, loc), + loc, + } satisfies IrStmt, + ] + : []), { kind: "return", value: varRef("out.0", outT, loc), loc }, ]; } else if (method === "filter" && fastT) { diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index b4e285ee9..a7ce0a106 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -97,11 +97,15 @@ export function arrayConcatHelper( return [ { kind: "varDecl", localId: "offset.0", init: length(out), loc }, { kind: "varDecl", localId: "count.0", init: length(source), loc }, + // Storage is reserved first and the length is set after the copy, so + // a source without holes is copied by appends and the result gets no + // state bytes. { kind: "arraySetLength", arr: out, length: { kind: "bin", op: "+", left: offset, right: count, type: F64, loc }, loc, + reserve: true, }, countedFor(loc, count, () => [ { @@ -134,6 +138,12 @@ export function arrayConcatHelper( loc, }, ]), + { + kind: "arraySetLength", + arr: out, + length: { kind: "bin", op: "+", left: offset, right: count, type: F64, loc }, + loc, + }, ]; }; const body: IrStmt[] = [ diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 180f40435..2f839a20b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1263,17 +1263,28 @@ class FusedLoop { loc, }); if (holes) { + // The result keeps the source's holes: storage for its length is + // reserved first and the length is set after the loop, so a result + // without holes is filled by appends and gets no state bytes. + const length = (): IrExpr => ({ + kind: "ternary", + cond: { kind: "bin", op: "<", left: lo, right: hi, type: BOOL, loc }, + then: { kind: "bin", op: "-", left: hi, right: lo, type: F64, loc }, + else_: numLit(0, loc), + type: F64, + loc, + }); stateDecls.push({ kind: "arraySetLength", arr: varRef(out, resultT, loc), - length: { - kind: "ternary", - cond: { kind: "bin", op: "<", left: lo, right: hi, type: BOOL, loc }, - then: { kind: "bin", op: "-", left: hi, right: lo, type: F64, loc }, - else_: numLit(0, loc), - type: F64, - loc, - }, + length: length(), + loc, + reserve: true, + }); + epilogue.push({ + kind: "arraySetLength", + arr: varRef(out, resultT, loc), + length: length(), loc, }); } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4a395feb2..8008008f9 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1943,8 +1943,18 @@ export type IrStmt = * RangeError from the runtime. `pop` marks the stores that implement * `pop()`: a frozen (published) array fails with pop's TypeError rather * than the length assignment's, and pop's `length = 0` store on an empty - * array is dropped unless the array may be frozen. */ - | { kind: "arraySetLength"; arr: IrExpr; length: IrExpr; loc: SrcLoc; pop?: boolean } + * array is dropped unless the array may be frozen. `reserve` marks a + * capacity hint instead: dense storage for `length` elements of a fresh + * array that statements fill next, with no change to its length and no + * observable effect. */ + | { + kind: "arraySetLength"; + arr: IrExpr; + length: IrExpr; + loc: SrcLoc; + pop?: boolean; + reserve?: boolean; + } /** Write an explicit present `undefined` state without shrinking length. */ | { kind: "arraySetUndefined"; arr: IrExpr; index: IrExpr; loc: SrcLoc } /** Delete an indexed/property slot without changing Array.length. */ diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c00d1b4f6..c63bc4cc9 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -48,7 +48,7 @@ static inline const ScrArrExt *scr_arr_ax(const ScrArr *a) { } #define SCR_AX(a) scr_arr_ax(a) -static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit); +static void scr_arr_move_inline(ScrArr *a, size_t cap); /* Bytes before the dense slots' allocation start: the ext pointer's word. */ static size_t scr_arr_prefix(const ScrArr *a) { @@ -60,9 +60,10 @@ static void *scr_arr_alloc_start(const ScrArr *a) { return (char *)scr_arr_block(a) - scr_arr_prefix(a); } -/* Bytes per dense slot with its state: an 8-byte value, or a boolean byte. */ +/* Bytes per dense slot: an 8-byte value or a boolean byte, and its state + * byte when the array has states. */ static size_t scr_arr_slot_unit(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 2 * sizeof(uint8_t) : sizeof(uint64_t) + sizeof(uint8_t); + return scr_arr_slot_width(a) + (scr_arr_has_states(a) ? 1 : 0); } /* Bytes of dense storage (head room included) after the prefix. */ @@ -78,7 +79,7 @@ ScrArrExt *scr_arr_ext(ScrArr *a) { /* The pointer goes in the word before the dense allocation, which an * array with ext always has: inline slots move out, and an array without * storage gets an allocation of the word alone. */ - if (a->spare & SCR_ARR_INLINE_SLOTS) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); + if (a->spare & SCR_ARR_INLINE_SLOTS) scr_arr_move_inline(a, a->cap); char *start; if (!a->data) { start = scr_mem_alloc(SCR_ARR_EXT_PREFIX); @@ -387,18 +388,14 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { x->sparse_len -= count; } -/* Bytes per dense slot: the value and its state byte. Boolean arrays - * store a one-byte value. */ -#define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) -#define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) /* Inline slots: an array built by scr_arr_new_small / scr_arr_new_ops_small * (empty literals the compiler sees appended to), or constructed with a * known element count of up to SCR_ARR_KNOWN_INLINE_MAX, keeps its first - * dense slots and states in its own block, right after the ScrArr, so it - * costs one allocation and its first elements share the header's cache lines. + * dense slots in its own block, right after the ScrArr, so it costs one + * allocation and its first elements share the header's cache lines. * `spare` records it. Growth past the inline capacity moves the slots to an * ordinary allocation; the inline room then stays unused until the array is * freed. */ @@ -449,8 +446,14 @@ static void scr_arr_drop_head(ScrArr *a) { a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; if (head == 0) return; size_t cap = a->cap, total = cap + head, live = a->len < cap ? a->len : cap; - uint8_t *end = scr_arr_states_end(a); uint64_t *data = a->data - head; + if (!scr_arr_has_states(a)) { + if (live) memmove(data, a->data, live * sizeof(uint64_t)); + a->data = data; + a->cap = total; + return; + } + uint8_t *end = scr_arr_states_end(a); /* The states end moves to the end of the allocation, past the `head` * vacated states: the live states move up with it, and every other state * becomes a hole. */ @@ -472,7 +475,7 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; if (count == 0) return true; size_t head = scr_arr_head(a); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_HOLE, count); memset(a->data, 0, count * sizeof(uint64_t)); if (head) a->data[-1] = 0; a->data += count; @@ -492,9 +495,9 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { } /* Slots requested by the small constructions. The small allocator rounds - * them up to fill its block: 3 eight-byte slots after a plain header in a - * 64-byte block (72 after a collector header), or 4 booleans in a 40-byte - * block. Most small arrays hold up to three elements; two slots make + * them up to fill its block: 4 eight-byte slots after a plain header in a + * 64-byte block (3 in 72 bytes after a collector header), or 8 booleans in + * a 40-byte block. Most small arrays hold up to three elements; two slots make * arrays that grow past them move their storage often enough to slow * programs that build many short lists. */ #if SCR_SMALL_ALLOC @@ -504,18 +507,14 @@ static bool scr_arr_drop_front(ScrArr *a, size_t count) { #endif /* Inline capacity of a small construction with `prefix` bytes of block - * before the slots. */ -static size_t scr_arr_small_cap(size_t prefix, bool bools) { + * before slots of `unit` bytes. Inlined, so it folds to a constant for a + * constant unit. */ +static inline size_t scr_arr_small_cap(size_t prefix, size_t unit) { #if SCR_SMALL_ALLOC - /* One return per slot unit, so each folds to a constant at its callers. */ - if (bools) - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_BOOL_SLOT_BYTES)) - prefix) / - SCR_ARR_BOOL_SLOT_BYTES; - return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_SLOT_BYTES)) - prefix) / - SCR_ARR_SLOT_BYTES; + return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * unit)) - prefix) / unit; #else (void)prefix; - (void)bools; + (void)unit; return SCR_ARR_SMALL_SLOTS; #endif } @@ -528,18 +527,17 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) { #define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16) /* Inline capacity of a construction of `need` elements with `prefix` bytes - * of block before the slots, rounded up to fill the block; 0 when its slots - * stay separate. */ -static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) { + * of block before slots of `unit` bytes, rounded up to fill the block; 0 + * when its slots stay separate. */ +static size_t scr_arr_known_cap(size_t prefix, size_t need, size_t unit) { if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0; #if SCR_SMALL_ALLOC - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; size_t bytes = prefix + need * unit; if (bytes > SCR_SA_MAX) return 0; return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit; #else (void)prefix; - (void)bools; + (void)unit; return need; #endif } @@ -547,20 +545,22 @@ static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) { static void scr_arr_use_inline(ScrArr *a, size_t cap) { a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET); a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } /* Move inline slots to their own allocation of `cap` (>= a->cap) slots; the * array's block keeps the unused room until it is freed. */ -static void scr_arr_move_inline(ScrArr *a, size_t cap, size_t unit) { +static void scr_arr_move_inline(ScrArr *a, size_t cap) { size_t width = scr_arr_slot_width(a); - uint64_t *data = scr_mem_alloc(cap * unit); + uint64_t *data = scr_mem_alloc(cap * scr_arr_slot_unit(a)); if (!data) scr_arr_oom(); - uint8_t *end = (uint8_t *)data + cap * (width + 1); memcpy(data, a->data, a->cap * width); - memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); - memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + if (scr_arr_has_states(a)) { + uint8_t *end = (uint8_t *)data + cap * (width + 1); + memcpy(end - a->cap, scr_arr_state_run(a, 0, a->cap), a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + } a->data = data; a->cap = cap; a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; @@ -582,15 +582,15 @@ typedef enum { * reallocation. Growth keeps its own sequence (4, 8, 16, ...): rounding it * as well moves array storage into other size classes and measured * slower. */ -static size_t scr_arr_block_cap(size_t cap, bool bools) { +static size_t scr_arr_block_cap(size_t cap, size_t unit) { #if SCR_SMALL_ALLOC - size_t bytes = cap * (bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); + size_t bytes = cap * unit; if (bytes <= SCR_SA_MAX) { size_t block = scr_sa_class_size(scr_sa_class((bytes + 15) & ~(size_t)15)); - return bools ? block / SCR_ARR_BOOL_SLOT_BYTES : block / SCR_ARR_SLOT_BYTES; + return block / unit; } #else - (void)bools; + (void)unit; #endif return cap; } @@ -614,22 +614,25 @@ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode scr_arr_grow_dense_slow(a, need, mode); return; } + bool states = scr_arr_has_states(a); + size_t unit = sizeof(uint64_t) + (states ? 1 : 0); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, false); + cap = scr_arr_block_cap(need, unit); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) cap += cap; if (cap > SCR_ARR_DENSE_LIMIT) cap = SCR_ARR_DENSE_LIMIT; } size_t old = a->cap; - uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * SCR_ARR_SLOT_BYTES) - : scr_mem_alloc(cap * SCR_ARR_SLOT_BYTES); + uint64_t *data = a->data ? scr_mem_realloc(a->data, cap * unit) : scr_mem_alloc(cap * unit); if (!data) scr_arr_oom(); - /* The states end the allocation: move them to its new end. */ - uint8_t *end = (uint8_t *)(data + cap) + cap; - if (old) memmove(end - old, (uint8_t *)(data + old), old); - memset(end - cap, SCR_ARR_HOLE, cap - old); + if (states) { + /* The states end the allocation: move them to its new end. */ + uint8_t *end = (uint8_t *)(data + cap) + cap; + if (old) memmove(end - old, (uint8_t *)(data + old), old); + memset(end - cap, SCR_ARR_HOLE, cap - old); + } a->data = data; a->cap = cap; } @@ -660,12 +663,12 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { mode = SCR_ARR_GROW_APPEND; } } - bool bools = a->elem == SCR_ELEM_BOOL; + bool states = scr_arr_has_states(a); size_t width = scr_arr_slot_width(a); - size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_arr_slot_unit(a); size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, bools); + cap = scr_arr_block_cap(need, unit); } else { /* Inline slots overflow into the ordinary sequence (4, 8, 16, ...). */ size_t from = scr_arr_inline(a) ? 0 : a->cap; @@ -680,9 +683,9 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { * appends benefit from geometric headroom. */ if (mode == SCR_ARR_GROW_SPAN && need > SCR_ARR_DENSE_LIMIT) cap = need; } - if (cap > SIZE_MAX / SCR_ARR_SLOT_BYTES) scr_arr_oom(); /* bounds both slot sizes */ + if (cap > SIZE_MAX / (sizeof(uint64_t) + 1)) scr_arr_oom(); /* bounds every slot unit */ if (scr_arr_inline(a)) { - scr_arr_move_inline(a, cap, unit); + scr_arr_move_inline(a, cap); scr_arr_absorb_sparse(a); return; } @@ -692,7 +695,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { if (!data) scr_arr_oom(); a->data = data; a->cap = (uint32_t)cap; - memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); + if (states) memset(scr_arr_state_run(a, 0, cap), SCR_ARR_HOLE, cap); return; } /* Values and states share one allocation. Move the old states to its new @@ -702,9 +705,11 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode) { char *start = scr_mem_realloc((char *)a->data - prefix, prefix + cap * unit); if (!start) scr_arr_oom(); uint64_t *data = (uint64_t *)(start + prefix); - uint8_t *end = (uint8_t *)data + cap * (width + 1); - if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); - memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + if (states) { + uint8_t *end = (uint8_t *)data + cap * (width + 1); + if (a->cap) memmove(end - a->cap, (uint8_t *)data + a->cap * width, a->cap); + memset(end - cap, SCR_ARR_HOLE, cap - a->cap); + } a->data = data; a->cap = cap; scr_arr_absorb_sparse(a); @@ -714,6 +719,44 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { scr_arr_grow_dense_to(a, need, SCR_ARR_GROW_APPEND); } +/* States follow the value slots, and the capacity stays: inline slots + * move to an allocation of their own, and other storage grows by the state + * bytes (immortal storage, which may be static, is copied instead). */ +void scr_arr_add_states(ScrArr *a) { + if (scr_arr_has_states(a)) return; + size_t width = scr_arr_slot_width(a), len = a->len; + if (a->cap == 0) { + a->spare |= SCR_ARR_STATES; + return; + } + if (scr_arr_inline(a)) { + uint64_t *data = scr_mem_alloc(a->cap * (width + 1)); + if (!data) scr_arr_oom(); + memcpy(data, a->data, len * width); + a->data = data; + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + } else if (a->rc == SIZE_MAX) { + size_t prefix = scr_arr_prefix(a); + char *start = scr_mem_alloc(prefix + a->cap * (width + 1)); + if (!start) scr_arr_oom(); + if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); + memcpy(start + prefix, a->data, len * width); + a->data = (uint64_t *)(start + prefix); + a->spare &= (uint16_t)~SCR_ARR_HEAD_ROOM; + } else { + /* Head room goes back to the capacity first. */ + scr_arr_drop_head(a); + size_t prefix = scr_arr_prefix(a); + char *start = scr_mem_realloc(scr_arr_alloc_start(a), prefix + a->cap * (width + 1)); + if (!start) scr_arr_oom(); + a->data = (uint64_t *)(start + prefix); + } + a->spare |= SCR_ARR_STATES; + uint8_t *end = scr_arr_states_end(a); + memset(end - len, SCR_ARR_VALUE, len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); +} + /* Room for `count` values in front of the first element of a dense array: * moves `data` back into head room, raising `cap`; the caller * stores the new first `count` slots and states. A shifted array reuses the @@ -734,21 +777,24 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { /* The new allocation: head room for the new values plus half the new * length, then the current dense window (its tail capacity included). */ size_t room = count + (len + count) / 2; - if (room > SCR_ARR_MAX_LENGTH || a->cap > SIZE_MAX / SCR_ARR_SLOT_BYTES - room) return false; + size_t unit = scr_arr_slot_unit(a); + if (room > SCR_ARR_MAX_LENGTH || a->cap > SIZE_MAX / unit - room) return false; size_t total = room + a->cap; size_t prefix = scr_arr_prefix(a); - char *start = scr_mem_alloc(prefix + total * SCR_ARR_SLOT_BYTES); + char *start = scr_mem_alloc(prefix + total * unit); if (!start) scr_arr_oom(); /* The ext pointer's word moves with the allocation. */ if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); uint64_t *block = (uint64_t *)(start + prefix); - /* The window's states end `room` bytes before the allocation does; the - * room's states past it are holes. */ - uint8_t *end = (uint8_t *)(block + total) + a->cap; memcpy(block + room, a->data, len * sizeof(uint64_t)); - memset(end, SCR_ARR_HOLE, room); - memcpy(end - len, scr_arr_state_run(a, 0, len), len); - memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); + if (scr_arr_has_states(a)) { + /* The window's states end `room` bytes before the allocation does; + * the room's states past it are holes. */ + uint8_t *end = (uint8_t *)(block + total) + a->cap; + memset(end, SCR_ARR_HOLE, room); + memcpy(end - len, scr_arr_state_run(a, 0, len), len); + memset(end - a->cap, SCR_ARR_HOLE, a->cap - len); + } scr_mem_free(scr_arr_alloc_start(a)); a->data = block + room; head = room; @@ -764,10 +810,9 @@ static bool scr_arr_open_head(ScrArr *a, size_t count) { * within that gap past capacity. */ static bool scr_arr_dense_tail_populated(const ScrArr *a) { size_t window = a->cap < SCR_ARR_DENSE_GAP ? a->cap : SCR_ARR_DENSE_GAP; - const uint8_t *states = scr_arr_states_end(a); - if (window == 0 || *scr_arr_state_in(states, a->cap - 1) == SCR_ARR_HOLE) return false; + if (window == 0 || scr_arr_dense_state(a, a->cap - 1) == SCR_ARR_HOLE) return false; size_t populated = 0; - for (size_t i = a->cap - window; i < a->cap; i++) populated += *scr_arr_state_in(states, i) != SCR_ARR_HOLE; + for (size_t i = a->cap - window; i < a->cap; i++) populated += scr_arr_dense_state(a, i) != SCR_ARR_HOLE; return populated * 16 >= window * 3; } @@ -841,7 +886,7 @@ static __attribute__((noinline, pure)) ScrArrStateSlot scr_arr_sparse_state_at(c * sparse entries above it. */ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t *out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_in(scr_arr_states_end(a), index); + uint8_t state = scr_arr_dense_state(a, index); if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; if (out) *out = scr_arr_dense_get(a, index); return state; @@ -855,15 +900,19 @@ static inline bool scr_arr_slot_at(const ScrArr *a, size_t index, uint64_t *out) return scr_arr_state_at(a, index, out) != SCR_ARR_HOLE; } -/* Store an owned slot into an empty index. The caller has already established - * the destination length and does not need a retain. */ +/* Store an owned slot into an empty index. The caller does not need a + * retain. An array with states has its destination length established + * already; an array without states takes only a value at its length, + * whose caller then raises the length, and gets states for any other + * store. */ static void scr_arr_store_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t state) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index != a->len)) scr_arr_add_states(a); scr_arr_grow_for_index(a, index); if (index < a->cap) { scr_arr_dense_put(a, index, slot); - *scr_arr_state_ptr(a, index) = state; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; return; } size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); @@ -879,12 +928,15 @@ static void scr_arr_store_owned(ScrArr *a, size_t index, uint64_t slot) { scr_arr_store_state_owned(a, index, slot, SCR_ARR_VALUE); } +/* Take the slot at `index`, leaving a hole: an array without states gets + * them. */ static bool scr_arr_take_state(ScrArr *a, size_t index, uint64_t *out, uint8_t *state_out) { if (index < a->cap) { - uint8_t state = *scr_arr_state_ptr(a, index); + uint8_t state = scr_arr_dense_state(a, index); if (state == SCR_ARR_HOLE) return false; scr_arr_cut(a, index); + scr_arr_need_states(a); *out = scr_arr_dense_get(a, index); *scr_arr_state_ptr(a, index) = SCR_ARR_HOLE; scr_arr_dense_put(a, index, 0); @@ -909,11 +961,19 @@ static void scr_arr_replace_state_owned(ScrArr *a, size_t index, uint64_t slot, uint8_t old_state; uint64_t old = 0; if (state != SCR_ARR_VALUE) scr_arr_cut(a, index); + /* Without states, a value below the length replaces a value. */ + if (!scr_arr_has_states(a) && (state != SCR_ARR_VALUE || index >= a->len)) { + if (state == SCR_ARR_VALUE && index == a->len) { + scr_arr_store_state_owned(a, index, slot, state); + return; + } + scr_arr_add_states(a); + } if (index < a->cap) { - old_state = *scr_arr_state_ptr(a, index); + old_state = scr_arr_dense_state(a, index); if (old_state == SCR_ARR_VALUE) old = scr_arr_dense_get(a, index); scr_arr_dense_put(a, index, slot); - *scr_arr_state_ptr(a, index) = state; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, index) = state; } else { size_t pos = scr_arr_sparse_lower_bound(SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, index); if (pos == SCR_AX(a)->sparse_len || SCR_AX(a)->sparse[pos].index != index) { @@ -941,6 +1001,10 @@ static void scr_arr_note_value(ScrArr *a, size_t index) { /* scr_arr_refill bounded to [0, limit): for range operations whose cost is * proportional to the range, not to the whole array. */ static void scr_arr_refill_to(ScrArr *a, size_t limit) { + if (!scr_arr_has_states(a)) { + scr_arr_refill(a); + return; + } size_t end = a->len < a->cap ? a->len : a->cap; if (limit < end) end = limit; size_t f = a->filled; @@ -1022,7 +1086,11 @@ static bool scr_arr_prop_delete(ScrArr *a, double key) { return true; } +/* Detach the storage of `a` for a rebuild through the element stores. Both + * have states: the detached storage for its readers, and the array for + * stores at any index. */ static ScrArrStorage scr_arr_take_storage(ScrArr *a) { + scr_arr_need_states(a); ScrArrExt *x = scr_arr_ext_of(a); ScrArrStorage old = {a->data, scr_arr_states_end(a), a->cap, SCR_AX(a)->sparse, SCR_AX(a)->sparse_len, SCR_AX(a)->sparse_cap, a->elem, @@ -1059,17 +1127,20 @@ static bool scr_arr_is_dense(const ScrArr *a) { return a->len <= a->cap && (!(a->spare & SCR_ARR_HAS_EXT) || scr_arr_ext_of(a)->sparse_len == 0); } +/* Without states, both move the values only: the caller keeps the + * elements contiguous from 0 and sets the length to their count. */ static void scr_arr_move_dense(ScrArr *a, size_t to, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, to); memmove(scr_arr_dense_at(a, to), scr_arr_dense_at(a, from), count * scr_arr_slot_width(a)); - memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); + if (scr_arr_has_states(a)) + memmove(scr_arr_state_run(a, to, count), scr_arr_state_run(a, from, count), count); } static void scr_arr_clear_dense(ScrArr *a, size_t from, size_t count) { if (count == 0) return; scr_arr_cut(a, from); - memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, count), SCR_ARR_HOLE, count); memset(scr_arr_dense_at(a, from), 0, count * scr_arr_slot_width(a)); } @@ -1138,10 +1209,20 @@ static inline void scr_arr_copy_slots(uint64_t *dst, const uint64_t *src, size_t } } +/* Whether dense slots [from, from + count) of `a` all hold values. */ +static bool scr_arr_dense_run_values(const ScrArr *a, size_t from, size_t count) { + if (!scr_arr_has_states(a)) return from + count <= a->len; + if (from + count <= a->filled) return true; + return scr_arr_value_run(scr_arr_states_end(a), from, from + count) == from + count; +} + /* The destination owns no values in this range. Only retain adapters run * while copying references: no release can collect an intermediate copy. * Self-appends use disjoint ranges after reserving capacity. Both arrays - * hold the same element kind, so their slots have the same width. */ + * hold the same element kind, so their slots have the same width. A + * destination without states keeps none for a range of values: its caller + * makes the range contiguous with its elements and sets the length; any + * other range gives it states. */ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, size_t from, size_t count, bool reverse, bool materialize_holes) { @@ -1149,28 +1230,49 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, scr_arr_cut(dst, to); /* 8-byte slots: a constant slot size, and constant-width state ends. */ bool wide = dst->elem != SCR_ELEM_BOOL; + /* Callers copy from an array without states only from its elements + * (which a self-copy reads after moving them, past the length). */ + bool values = !scr_arr_has_states(src) || scr_arr_dense_run_values(src, from, count); + if (!values) scr_arr_need_states(dst); + bool refs = scr_elem_is_ref(src->elem); + if (!scr_arr_has_states(dst)) { + if (!reverse && wide) { + scr_arr_copy_slots(dst->data + to, src->data + from, count); + } else if (!reverse) { + memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * scr_arr_slot_width(dst)); + } else { + for (size_t i = 0; i < count; i++) + scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, from + count - i - 1)); + } + if (!refs) return; + uint8_t elem = src->elem; + const ScrArrOps *ops = scr_arr_ops(src); + for (size_t i = to; i < to + count; i++) dst->data[i] = scr_elem_retain_with(elem, ops, dst->data[i]); + return; + } uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, 1); - const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, 1); + const uint8_t *src_states = values ? NULL : wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, 1); if (!reverse && wide) { scr_arr_copy_slots(dst->data + to, src->data + from, count); - /* A range below the source's hole-free prefix holds only values. */ - if (from + count <= src->filled) + if (values) scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); else scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else if (!reverse) { memcpy(scr_arr_dense_at(dst, to), scr_arr_dense_at(src, from), count * scr_arr_slot_width(dst)); - scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); + if (values) + scr_arr_set_states(dst_states - to - count, SCR_ARR_VALUE, count); + else + scr_arr_copy_states(dst_states - to - count, src_states - from - count, count); } else { for (size_t i = 0; i < count; i++) { size_t source = from + count - i - 1; scr_arr_dense_put(dst, to + i, scr_arr_dense_get(src, source)); - *scr_arr_state_in(dst_states, to + i) = *scr_arr_state_in(src_states, source); + *scr_arr_state_in(dst_states, to + i) = values ? SCR_ARR_VALUE : *scr_arr_state_in(src_states, source); } } - bool refs = scr_elem_is_ref(src->elem); - if (!refs && !materialize_holes) return; + if (!refs && (!materialize_holes || values)) return; uint8_t elem = src->elem; const ScrArrOps *ops = scr_arr_ops(src); for (size_t i = to; i < to + count; i++) { @@ -1197,9 +1299,9 @@ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, bool small) { - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; - size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) - : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL); + size_t unit = scr_elem_slot_width(elem); + size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit) + : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit); ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); if (!a) scr_arr_oom(); a->rc = 1; @@ -1225,13 +1327,18 @@ ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true); } /* A non-cycle array whose final length the runtime knows when it builds - * it (regex match results): its `cap` slots and states share the header's - * block, so construction and teardown cost one allocation. Growth past + * it (regex match results): its `cap` slots share the header's block, so + * construction and teardown cost one allocation. With `states`, the array + * starts with states, whose bytes share the block as well. Growth past * `cap` moves the slots out like any inline array. */ -ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { - if (cap == 0) return scr_arr_make(elem, 0, false); +ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states) { + if (cap == 0) { + ScrArr *a = scr_arr_make(elem, 0, false); + if (states) a->spare |= SCR_ARR_STATES; + return a; + } if (cap > SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; + size_t unit = scr_elem_slot_width(elem) + (states ? 1 : 0); ScrArr *a = scr_mem_alloc(SCR_ARR_INLINE_OFFSET + cap * unit); if (!a) scr_arr_oom(); a->rc = 1; @@ -1239,7 +1346,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->cap = 0; a->elem = (uint8_t)elem; a->cyc = 0; - a->spare = 0; + a->spare = states ? SCR_ARR_STATES : 0; a->filled = 0; a->data = NULL; scr_arr_use_inline(a, cap); @@ -1250,10 +1357,9 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { } void scr_arr_outline(ScrArr *a) { - if (scr_arr_inline(a)) - scr_arr_move_inline(a, a->cap, - a->elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES); - /* The swapped fields do not include `spare`: no head room either. */ + if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap); + /* Of `spare`, a swap of the storage fields may move only the states + * flag: no head room either. */ if (a->spare & SCR_ARR_HEAD_ROOM) { scr_arr_drop_head(a); scr_arr_absorb_sparse(a); @@ -1287,9 +1393,14 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { if (SCR_AX(a)->metadata) visit(SCR_AX(a)->metadata, ctx); /* Growth and truncation leave spare capacity empty. Sparse slots and * named properties are separate edges, independent of dense length. */ - const uint8_t *states = scr_arr_states_end(a); - for (size_t i = 0, end = a->len < a->cap ? a->len : a->cap; i < end; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + size_t end = a->len < a->cap ? a->len : a->cap; + if (!scr_arr_has_states(a)) { + for (size_t i = 0; i < end; i++) visit(scr_slot_to_ptr(a->data[i]), ctx); + } else { + const uint8_t *states = scr_arr_states_end(a); + for (size_t i = 0; i < end; i++) { + if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) visit(scr_slot_to_ptr(a->data[i]), ctx); + } } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) { @@ -1338,9 +1449,10 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA ScrArr *a; bool cyc = ops->trace != NULL; size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); + size_t unit = scr_elem_slot_width(SCR_ELEM_REF); size_t inline_cap = - small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false); - size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr); + small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit); + size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr); if (cyc) { static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; a = scr_cyc_alloc_inline(size, &cyc_type); @@ -1391,18 +1503,28 @@ static void scr_arr_destroy(void *object) { * first (and the storage cannot move under the loop); immortal strings * (interned characters, literals) need no call. */ const uint64_t *data = a->data; - const uint8_t *states = scr_arr_states_end_w(a, 8); - /* Slots below the hole-free prefix bound hold values. */ - size_t filled = a->filled < end ? a->filled : end; + /* Slots below the hole-free prefix bound hold values, as do all + * slots below the length of an array without states. */ + size_t filled = !scr_arr_has_states(a) ? end : a->filled < end ? a->filled : end; for (size_t i = 0; i < filled; i++) { ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } + const uint8_t *states = scr_arr_states_end_w(a, 8); for (size_t i = filled; i < end; i++) { if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; ScrStr *s = (ScrStr *)scr_slot_to_ptr(data[i]); if (s && s->rc != SIZE_MAX) scr_str_release(s); } + } else if (!scr_arr_has_states(a)) { + /* Each slot is cleared before its release, as with states. */ + uint8_t elem = a->elem; + const ScrArrOps *ops = scr_arr_ops(a); + for (size_t i = 0; i < end; i++) { + uint64_t old = a->data[i]; + a->data[i] = 0; + scr_elem_release_with(elem, ops, old); + } } else { uint8_t *states = scr_arr_states_end(a); uint8_t elem = a->elem; @@ -1485,6 +1607,7 @@ double scr_arr_next_present(const ScrArr *a, double start) { if (!(start < SCR_ARR_LEN_F64(a->len))) return SCR_ARR_LEN_F64(a->len); size_t i = start > 0 ? (size_t)ceil(start) : 0; size_t end = a->cap < a->len ? a->cap : a->len; + if (!scr_arr_has_states(a)) return (double)i; const uint8_t *states = scr_arr_states_end(a); for (; i < end; i++) { if (*scr_arr_state_in(states, i) != SCR_ARR_HOLE) return (double)i; @@ -1523,6 +1646,22 @@ void scr_arr_set_len(ScrArr *a, double length) { * array must cost only the elements removed, not its historical peak. */ size_t dense_end = a->len < a->cap ? a->len : a->cap; size_t dense_stop = next < dense_end ? next : dense_end; + if (!scr_arr_has_states(a)) { + /* Values only: each slot is cleared before its release. The + * length drops first, so a release that reads the array sees the + * values that remain. */ + a->len = (uint32_t)next; + if (scr_elem_is_ref(a->elem)) { + for (size_t i = dense_stop; i < dense_end; i++) { + uint64_t old = a->data[i]; + a->data[i] = 0; + scr_elem_release(a, old); + } + } else if (dense_stop < dense_end) { + memset(scr_arr_dense_at(a, dense_stop), 0, (dense_end - dense_stop) * scr_arr_slot_width(a)); + } + return; + } if (scr_elem_is_ref(a->elem)) { for (size_t i = dense_stop; i < dense_end; i++) { if (*scr_arr_state_ptr(a, i) != SCR_ARR_HOLE) { @@ -1551,8 +1690,20 @@ void scr_arr_set_len(ScrArr *a, double length) { } if (scr_arr_ext_of(a)) scr_arr_ext_of(a)->sparse_len = scr_arr_sparse_lower_bound(scr_arr_ext_of(a)->sparse, scr_arr_ext_of(a)->sparse_len, next); } - } else if (a->len == 0 && next <= SCR_ARR_PRESIZE_LIMIT && (a->cap == 0 || scr_arr_inline(a))) { - scr_arr_reserve_exact(a, next); + } else if (next > a->len) { + /* The indices past the elements are holes. An empty array gives up + * its inline slots first, so its slots and states take one allocation. */ + if (a->len == 0 && next <= SCR_ARR_PRESIZE_LIMIT && (a->cap == 0 || scr_arr_inline(a))) { + if (scr_arr_inline(a)) { + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + a->cap = 0; + a->data = NULL; + } + scr_arr_need_states(a); + scr_arr_reserve_exact(a, next); + } else { + scr_arr_need_states(a); + } } a->len = next; } @@ -1583,11 +1734,24 @@ void scr_arr_copy_range_ex(ScrArr *dst, size_t dst_start, const ScrArr *src, scr_arr_refill_to(dst, dst_start + count); } +/* A destination without states takes a dense copy at its length, whose + * caller then raises the length over the copy; any other copy gives it + * states first. */ static void scr_arr_copy_range_states(ScrArr *dst, size_t dst_start, const ScrArr *src, size_t src_start, size_t count, bool reverse, bool materialize_holes) { - if (src != dst && src_start <= src->cap && count <= src->cap - src_start && - dst_start <= dst->cap && count <= dst->cap - dst_start) { + size_t src_end = scr_arr_has_states(src) ? src->cap : src->len; + bool fits = src != dst && src_start <= src_end && count <= src_end - src_start && + dst_start <= dst->cap && count <= dst->cap - dst_start; + if (!scr_arr_has_states(dst)) { + if (fits && dst_start == dst->len) { + scr_arr_copy_dense(dst, dst_start, src, src_start, count, reverse, materialize_holes); + return; + } + scr_arr_add_states(dst); + fits = fits && dst_start <= dst->cap && count <= dst->cap - dst_start; + } + if (fits) { const uint8_t *dst_states = scr_arr_states_end(dst); size_t empty = 0; while (empty < count && *scr_arr_state_in(dst_states, dst_start + empty) == SCR_ARR_HOLE) empty++; @@ -1673,11 +1837,12 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (from >= until) return scr_arr_retain(a); bool refs = scr_elem_is_ref(a->elem); if (scr_arr_dense_ok(a, until, until - from)) scr_arr_grow_dense(a, until); + if (state != SCR_ARR_VALUE) scr_arr_need_states(a); if (!refs && until <= a->cap) { if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); else for (size_t i = from; i < until; i++) a->data[i] = slot; - memset(scr_arr_state_run(a, from, until - from), state, until - from); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { for (size_t i = from; i < until; i++) { uint64_t value = refs && state == SCR_ARR_VALUE ? scr_elem_retain_slot(a, slot) : slot; @@ -1831,7 +1996,7 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { size_t idx = (size_t)i; if ((double)idx == i) { if (idx < a->filled) return scr_slot_to_ptr(a->data[idx]); - if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; + if (idx >= a->len || scr_arr_dense_state(a, idx) != SCR_ARR_VALUE) return NULL; return scr_slot_to_ptr(a->data[idx]); } } @@ -1861,8 +2026,8 @@ bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j) { if (i >= 0 && i < (double)a->cap && i == trunc(i) && j >= 0 && j < (double)b->cap && j == trunc(j)) { size_t ai = (size_t)i, bi = (size_t)j; - bool ap = ai < a->len && *scr_arr_state_ptr(a, ai) == SCR_ARR_VALUE; - bool bp = bi < b->len && *scr_arr_state_ptr(b, bi) == SCR_ARR_VALUE; + bool ap = ai < a->len && scr_arr_dense_state(a, ai) == SCR_ARR_VALUE; + bool bp = bi < b->len && scr_arr_dense_state(b, bi) == SCR_ARR_VALUE; if (!ap || !bp) return ap == bp; av = scr_arr_dense_get(a, ai); bv = scr_arr_dense_get(b, bi); @@ -1902,6 +2067,17 @@ static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot) { scr_arr_prop_set(a, i, slot); return; } + if (!scr_arr_has_states(a) && idx >= a->len) { + /* An append keeps the array without states; a write past the length + * leaves holes. */ + if (idx == a->len) { + scr_arr_store_owned(a, idx, slot); + a->len = (uint32_t)(idx + 1); + scr_arr_note_value(a, idx); + return; + } + scr_arr_add_states(a); + } if (idx >= a->len) a->len = idx + 1; /* Unlink-then-release: a release can trigger a cycle collection, which * must never see a heap edge whose count was already given up. */ @@ -1927,6 +2103,7 @@ void scr_arr_set_undefined(ScrArr *a, double i) { scr_arr_prop_set_undefined(a, i); return; } + scr_arr_need_states(a); if (idx >= a->len) a->len = idx + 1; scr_arr_replace_state_owned(a, idx, 0, SCR_ARR_UNDEFINED); } @@ -1976,7 +2153,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { scr_arr_grow_dense(a, next); if (count) { scr_arr_put_slots(a, from, count, slots); - scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) scr_arr_set_states(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } a->len = (uint32_t)next; /* The appended states are values: a bound at the old length moves to @@ -1985,6 +2162,7 @@ double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots) { else scr_arr_refill(a); return (double)next; } + scr_arr_need_states(a); for (size_t i = 0; i < count; i++) scr_arr_store_owned(a, from + i, slots[i]); a->len = (uint32_t)next; scr_arr_refill(a); @@ -2021,6 +2199,7 @@ double scr_arr_push_spread(ScrArr *a, const ScrArr *src) { scr_arr_refill(a); return SCR_ARR_LEN_F64(a->len); } + scr_arr_need_states(a); a->len = old_len + add; for (size_t i = 0; i < add; i++) { uint64_t slot; @@ -2065,9 +2244,10 @@ double scr_arr_concat_copy(ScrArr *a, const ScrArr *src) { scr_arr_refill(a); return SCR_ARR_LEN_F64(a->len); } + scr_arr_need_states(a); a->len = old_len + add; for (size_t i = 0; i < src->cap; i++) { - if (*scr_arr_state_ptr(src, i) != SCR_ARR_HOLE) { + if (scr_arr_dense_state(src, i) != SCR_ARR_HOLE) { scr_arr_copy_index(a, old_len + i, src, i); } } @@ -2121,7 +2301,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { /* The prefix stays put in front of the new first value. */ size_t f = a->filled; a->data[0] = slot; - *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -2132,7 +2312,7 @@ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { size_t f = a->filled; scr_arr_move_dense(a, 1, 0, old_len); scr_arr_dense_put(a, 0, slot); - *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, 0) = SCR_ARR_VALUE; a->len++; a->filled = (uint32_t)(f + 1); return SCR_ARR_LEN_F64(a->len); @@ -2173,7 +2353,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { if (scr_arr_is_dense(a) && scr_arr_open_head(a, count)) { size_t f = a->filled; scr_arr_put_slots(a, 0, count, slots); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -2183,7 +2363,7 @@ double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots) { size_t f = a->filled; scr_arr_move_dense(a, count, 0, old_len); scr_arr_put_slots(a, 0, count, slots); - memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); a->len = old_len + count; a->filled = (uint32_t)(f + count); return SCR_ARR_LEN_F64(a->len); @@ -2210,6 +2390,10 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { size_t add = src->len; if (add == 0) return (double)old_len; if (add > SCR_ARR_MAX_LENGTH - old_len) scr_arr_oom(); + /* Holes and present undefined in the source become present undefined: + * the receiver needs states, before its elements move. */ + if (!scr_arr_has_states(a) && (!scr_arr_is_dense(src) || !scr_arr_dense_run_values(src, 0, add))) + scr_arr_add_states(a); if (scr_arr_is_dense(a) && scr_arr_is_dense(src) && scr_arr_open_head(a, add)) { /* A self-spread reads the original values behind the opened room. */ scr_arr_copy_dense(a, 0, src, src == a ? add : 0, add, false, true); @@ -2257,6 +2441,15 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { ScrArr *scr_arr_reverse(ScrArr *a) { size_t len = a->len; + if (!scr_arr_has_states(a)) { + for (size_t i = 0; i < len / 2; i++) { + size_t j = len - i - 1; + uint64_t slot = scr_arr_dense_get(a, i); + scr_arr_dense_put(a, i, scr_arr_dense_get(a, j)); + scr_arr_dense_put(a, j, slot); + } + return scr_arr_retain(a); + } if (scr_arr_is_dense(a)) { /* A hole-free array stays hole-free; any other prefix is recomputed. */ if (a->filled != len) scr_arr_cut(a, 0); @@ -2292,6 +2485,13 @@ uint8_t scr_arr_pop_state(ScrArr *a, uint64_t *slot_out) { if (slot_out) *slot_out = 0; if (a->len == 0) return SCR_ARR_HOLE; size_t idx = a->len - 1; + if (!scr_arr_has_states(a)) { + if (slot_out) *slot_out = scr_arr_dense_get(a, idx); + scr_arr_dense_put(a, idx, 0); + scr_arr_cut(a, idx); + a->len = (uint32_t)idx; + return SCR_ARR_VALUE; + } uint64_t slot = 0; uint8_t state = SCR_ARR_HOLE; if (!scr_arr_take_state(a, idx, &slot, &state)) state = SCR_ARR_HOLE; @@ -2327,7 +2527,7 @@ uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out) { if (a->len == 0) return SCR_ARR_HOLE; size_t old_len = a->len; if (scr_arr_is_dense(a)) { - uint8_t state = *scr_arr_state_ptr(a, 0); + uint8_t state = scr_arr_dense_state(a, 0); if (slot_out && state == SCR_ARR_VALUE) *slot_out = scr_arr_dense_get(a, 0); /* The first slot becomes head room; the tail stays where it is. */ if (scr_arr_drop_front(a, 1)) return state; @@ -2398,13 +2598,15 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); - out->len = n; size_t old_len = a->len; if (scr_arr_is_dense(a)) { if (n) { + /* The removed elements keep their states unless all are values. */ + if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(out); memcpy(out->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); + if (scr_arr_has_states(out)) memcpy(scr_arr_state_run(out, 0, n), scr_arr_state_run(a, from, n), n); } + out->len = n; scr_arr_refill(out); /* A removed prefix becomes head room. */ if (from == 0 && scr_arr_drop_front(a, n)) return out; @@ -2418,6 +2620,8 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { else scr_arr_refill(a); return out; } + scr_arr_add_states(out); + out->len = n; ScrArrStorage old = scr_arr_take_storage(a); a->len = 0; for (size_t i = 0; i < old.cap; i++) { @@ -2475,12 +2679,17 @@ ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount, * is needed only by direct self-spread runtime callers. */ ScrArr *snapshot = items == a ? scr_arr_slice(a, 0, INFINITY) : NULL; if (snapshot) items = snapshot; + /* Holes and present undefined among the items become present + * undefined: the receiver needs states, before its elements move. */ + if (!scr_arr_dense_run_values(items, 0, add)) scr_arr_need_states(a); ScrArr *removed = a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { + if (!scr_arr_dense_run_values(a, from, n)) scr_arr_add_states(removed); memcpy(removed->data, scr_arr_dense_at(a, from), n * scr_arr_slot_width(a)); - memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); + if (scr_arr_has_states(removed)) + memcpy(scr_arr_state_run(removed, 0, n), scr_arr_state_run(a, from, n), n); } removed->len = n; scr_arr_refill(removed); @@ -2538,9 +2747,8 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou removed = scr_mem_alloc(n * sizeof(uint64_t)); if (!removed) scr_arr_oom(); } - const uint8_t *states = scr_arr_states_end(a); for (size_t i = from; i < from + n; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; + if (scr_arr_dense_state(a, i) == SCR_ARR_VALUE) removed[kept++] = a->data[i]; } } size_t old_len = a->len; @@ -2555,7 +2763,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou scr_arr_move_dense(a, from + count, from + n, old_len - from - n); if (count) { scr_arr_put_slots(a, from, count, slots); - memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, count), SCR_ARR_VALUE, count); } if (next_len < old_len) scr_arr_clear_dense(a, next_len, old_len - next_len); a->len = next_len; @@ -2580,13 +2788,15 @@ ScrArr *scr_arr_flat_copy(const ScrArr *a, ScrArr *out, bool flatten) { for (size_t j = 0; j < inner->len; j++) { if (scr_arr_state_at(inner, j, NULL) == SCR_ARR_HOLE) continue; if (out->len == SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t at = out->len++; + size_t at = out->len; scr_arr_copy_index(out, at, inner, j); + out->len = (uint32_t)(at + 1); } } else { if (out->len == SCR_ARR_MAX_LENGTH) scr_arr_oom(); - size_t at = out->len++; + size_t at = out->len; scr_arr_copy_index(out, at, a, i); + out->len = (uint32_t)(at + 1); } } scr_arr_refill(out); @@ -2611,10 +2821,12 @@ static bool scr_arr_ref_eq(const ScrArr *a, uint64_t slot, void *v) { return p == v; } +#define SCR_ARR_SCAN_VALUE(states, i) (!(states) || *scr_arr_state_in((states), (i)) == SCR_ARR_VALUE) + double scr_arr_index_of_f64(ScrArr *a, double v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_slot_to_f64(a->data[i]) == v) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && scr_slot_to_f64(a->data[i]) == v) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_slot_to_f64(SCR_AX(a)->sparse[i].slot) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2623,9 +2835,9 @@ double scr_arr_index_of_f64(ScrArr *a, double v) { } double scr_arr_index_of_bool(ScrArr *a, bool v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && ((scr_arr_dense_get(a, i) != 0) == v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && (SCR_AX(a)->sparse[i].slot != 0) == v) return (double)SCR_AX(a)->sparse[i].index; @@ -2634,9 +2846,9 @@ double scr_arr_index_of_bool(ScrArr *a, bool v) { } double scr_arr_index_of_ref(ScrArr *a, void *v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; + if (SCR_ARR_SCAN_VALUE(states, i) && scr_arr_ref_eq(a, a->data[i], v)) return (double)i; } for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) { if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE && scr_arr_ref_eq(a, SCR_AX(a)->sparse[i].slot, v)) return (double)SCR_AX(a)->sparse[i].index; @@ -2645,9 +2857,9 @@ double scr_arr_index_of_ref(ScrArr *a, void *v) { } bool scr_arr_includes_f64(ScrArr *a, double v) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; + if (!SCR_ARR_SCAN_VALUE(states, i)) continue; double x = scr_slot_to_f64(a->data[i]); if (x == v || (x != x && v != v)) return true; /* SameValueZero: NaN hits */ } @@ -2667,9 +2879,9 @@ bool scr_arr_includes_ref(ScrArr *a, void *v) { if (a->elem == SCR_ELEM_REF && scr_arr_ops(a)->retain == scr_dyn_retain_v) { const ScrDyn *needle = (const ScrDyn *)v; if (needle->kind == SCR_DYN_NUM && needle->v.num != needle->v.num) { - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < a->cap && i < a->len; i++) { - if (*scr_arr_state_in(states, i) != SCR_ARR_VALUE) continue; + if (!SCR_ARR_SCAN_VALUE(states, i)) continue; const ScrDyn *d = (const ScrDyn *)scr_slot_to_ptr(a->data[i]); if (d->kind == SCR_DYN_NUM && d->v.num != d->v.num) return true; } @@ -2730,22 +2942,26 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); - out->len = n; if (to <= a->cap && to <= a->filled && a->elem != SCR_ELEM_BOOL && !scr_elem_is_ref(a->elem)) { /* Numbers below the source's hole-free prefix: values only. */ scr_arr_copy_slots(out->data, a->data + from, n); - scr_arr_set_states(scr_arr_states_end_w(out, 8) - n, SCR_ARR_VALUE, n); + out->len = (uint32_t)n; out->filled = (uint32_t)n; return out; } if (to <= a->cap) { scr_arr_copy_dense(out, 0, a, from, n, false, false); + out->len = (uint32_t)n; /* States copied from below the source's bound are present values. */ if (n > 0 && from < a->filled) out->filled = (uint32_t)((to < a->filled ? to : a->filled) - from); scr_arr_refill(out); return out; } + /* A range reaching the sparse entries: the source has states, and so + * does the copy. */ + scr_arr_add_states(out); + out->len = (uint32_t)n; const uint8_t *states = scr_arr_states_end(a); for (size_t i = 0; i < a->cap && i < to; i++) { if (i < from || *scr_arr_state_in(states, i) == SCR_ARR_HOLE) continue; @@ -2875,8 +3091,9 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->len == 0) return copy ? scr_arr_new(a->elem, 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_states_end(a); - for (size_t i = 0; i < dense; i++) { + const uint8_t *states = scr_arr_scan_states(a); + if (!states) count = dense; + for (size_t i = 0; states && i < dense; i++) { count += *scr_arr_state_in(states, i) == SCR_ARR_VALUE; undefined_count += *scr_arr_state_in(states, i) == SCR_ARR_UNDEFINED; } @@ -2889,7 +3106,7 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (count && !values) scr_arr_oom(); size_t used = 0; for (size_t i = 0; i < dense; i++) - if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) values[used++] = scr_arr_dense_get(a, i); + if (SCR_ARR_SCAN_VALUE(states, i)) values[used++] = scr_arr_dense_get(a, i); for (size_t i = 0; i < SCR_AX(a)->sparse_len; i++) if (SCR_AX(a)->sparse[i].state == SCR_ARR_VALUE) values[used++] = SCR_AX(a)->sparse[i].slot; ScrNumberSortKey *numbers = NULL; @@ -2911,8 +3128,10 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { } size_t present = copy ? a->len : count + undefined_count; ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; - if (copy) out->len = a->len; - if (!copy && scr_arr_is_dense(a)) { + if (!copy && !states) { + /* Values only, in place. */ + if (count) scr_arr_put_slots(a, 0, count, values); + } else if (!copy && scr_arr_is_dense(a)) { if (count) scr_arr_put_slots(a, 0, count, values); scr_arr_cut(a, count); memset(scr_arr_state_run(a, 0, count), SCR_ARR_VALUE, count); @@ -2920,14 +3139,20 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { scr_arr_clear_dense(a, count + undefined_count, a->len - count - undefined_count); if (undefined_count) memset(scr_arr_dense_at(a, count), 0, undefined_count * scr_arr_slot_width(a)); + } else if (copy) { + /* The values, then present undefined for every other index. */ + for (size_t i = 0; i < count; i++) values[i] = scr_elem_retain_slot(a, values[i]); + scr_arr_push_many(out, count, values); + for (size_t i = count; i < present; i++) { + scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); + out->len = (uint32_t)(i + 1); + } } else { - ScrArrStorage old = {0}; - if (!copy) old = scr_arr_take_storage(a); - for (size_t i = 0; i < count; i++) - scr_arr_store_owned(out, i, copy ? scr_elem_retain_slot(a, values[i]) : values[i]); + ScrArrStorage old = scr_arr_take_storage(a); + for (size_t i = 0; i < count; i++) scr_arr_store_owned(out, i, values[i]); for (size_t i = count; i < present; i++) scr_arr_store_state_owned(out, i, 0, SCR_ARR_UNDEFINED); - if (!copy) scr_arr_free_storage(&old); + scr_arr_free_storage(&old); } free(values); scr_arr_refill(out); @@ -4606,7 +4831,7 @@ void scr_arr_sort_values(ScrArr *a, double count, ScrClosure *f, uint32_t arity, * holes, no storage-level undefined and no sparse tail. */ static bool scr_arr_dense_values(const ScrArr *a) { if (!scr_arr_is_dense(a)) return false; - if (a->filled == a->len) return true; + if (a->filled == a->len || !scr_arr_has_states(a)) return true; const uint8_t *states = scr_arr_states_end(a); bool all = true; for (size_t i = 0; i < a->len; i++) all &= *scr_arr_state_in(states, i) == SCR_ARR_VALUE; @@ -4683,16 +4908,16 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } else if (a->len >= n && a->cap >= n) { if (!retained) { memcpy(a->data, src, n * sizeof(*src)); - memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); + if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); } else { /* Publish each new edge before releasing the value it replaces: a * release can collect cycles. The buffer still holds the slots not * yet written. */ for (size_t i = 0; i < n; i++) { - bool held = *scr_arr_state_ptr(a, i) == SCR_ARR_VALUE; + bool held = scr_arr_dense_state(a, i) == SCR_ARR_VALUE; uint64_t old = held ? a->data[i] : 0; a->data[i] = src[i]; - *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; + if (scr_arr_has_states(a)) *scr_arr_state_ptr(a, i) = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6c41b3dbd..697912934 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -82,7 +82,7 @@ typedef struct ScrArr { * header. Kept beside `elem` so the RC fast paths test it with one load. */ uint8_t cyc; /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, - * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT). The rest: the + * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES). The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind * but SCR_ELEM_REF. */ uint16_t spare; @@ -94,8 +94,9 @@ typedef struct ScrArr { * 0 is always valid. */ uint32_t filled; /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL - * (scr_arr_slot_width), followed in the same allocation by one state byte - * per slot in reverse slot order (scr_arr_states_end). Sparse entries and properties keep 8-byte + * (scr_arr_slot_width), followed in the same allocation, when the array + * has states (SCR_ARR_STATES), by one state byte per slot in reverse slot + * order (scr_arr_states_end). Sparse entries and properties keep 8-byte * slots for every kind; a bool slot is 0 or 1 wherever it lives. Rarely * used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line, and an array that has it @@ -124,14 +125,17 @@ typedef struct ScrArrExt { /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like * scr_arr_retain and scr_arr_release; its inline element access uses len and - * cap (fields 1 and 2, i32), filled (field 6, i32) and data (field 7), and - * finds the state of slot i at data + cap * (slot width + 1) - 1 - i. */ + * cap (fields 1 and 2, i32), spare (field 5, i16, for SCR_ARR_STATES), + * filled (field 6, i32) and data (field 7), and finds the state of slot i + * of an array with states at data + cap * (slot width + 1) - 1 - i. */ _Static_assert(offsetof(ScrArr, len) == sizeof(size_t) && sizeof(((ScrArr *)0)->len) == 4, "emitted length reads load ScrArr.len as %ScrArr field 1, an i32"); _Static_assert(offsetof(ScrArr, cap) == sizeof(size_t) + 4 && sizeof(((ScrArr *)0)->cap) == 4, "emitted capacity reads load ScrArr.cap as %ScrArr field 2, an i32"); _Static_assert(offsetof(ScrArr, cyc) == sizeof(size_t) + 9, "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); +_Static_assert(offsetof(ScrArr, spare) == sizeof(size_t) + 10, + "inline element access reads ScrArr.spare as %ScrArr field 5"); _Static_assert(offsetof(ScrArr, filled) == sizeof(size_t) + 12, "inline element access reads ScrArr.filled as %ScrArr field 6"); _Static_assert(offsetof(ScrArr, data) == sizeof(size_t) + 16, @@ -145,11 +149,21 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), * count. Unshifted: an unshift had to move every element. Has ext: the word * before the dense allocation (head room included) points at the array's * ScrArrExt; such an array always has an allocation of its own, without - * inline slots. */ + * inline slots. States: the dense slots carry state bytes (see below). */ #define SCR_ARR_INLINE_SLOTS 1u #define SCR_ARR_HEAD_ROOM 2u #define SCR_ARR_UNSHIFTED 4u #define SCR_ARR_HAS_EXT 8u +#define SCR_ARR_STATES 16u + +/* An array starts without state bytes. Without them, every index below + * `len` is a present value in dense storage: len <= cap, there are no + * sparse entries, and `filled` equals `len` whenever emitted code can read + * the array (its inline reads load a state only at or past `filled`). The + * first hole or present `undefined` below the length, + * a length set past the elements, or a sparse entry gives the array its + * states (scr_arr_add_states), which it keeps. */ +static inline bool scr_arr_has_states(const ScrArr *a) { return (a->spare & SCR_ARR_STATES) != 0; } /* The start of the dense slots' allocation: `data` less any head room. */ static inline uint64_t *scr_arr_block(const ScrArr *a) { @@ -173,7 +187,7 @@ static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) { * Id 0 is no ops; ids are assigned on each descriptor's first array, in * any thread. A program with more element types than the field holds keeps * the ops of the rest in the array's ext (SCR_ARR_OPS_OVERFLOW). */ -#define SCR_ARR_OPS_SHIFT 4 +#define SCR_ARR_OPS_SHIFT 5 #define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT) extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW]; /* Assigns the id on a descriptor's first array; out of line, so the id @@ -197,17 +211,20 @@ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) { return scr_arr_ops_table[id]; } -/* Bytes per dense value slot. */ -static inline size_t scr_arr_slot_width(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); +/* Bytes per dense value slot of element kind `elem`. */ +static inline size_t scr_elem_slot_width(uint8_t elem) { + return elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); } -/* The dense states, one byte per slot, share the values' allocation (inline - * slots included). They follow the `cap` value slots in reverse slot order: - * the state of slot i is the byte i + 1 before scr_arr_states_end(a). Removing - * slots from the front (head room: `data` advances, `cap` drops) leaves every - * remaining state at its address. The address is computed on integers, so a - * NULL `data` stays NULL. */ +static inline size_t scr_arr_slot_width(const ScrArr *a) { return scr_elem_slot_width(a->elem); } + +/* The dense states of an array with states, one byte per slot, share the + * values' allocation (inline slots included). They follow the `cap` value + * slots in reverse slot order: the state of slot i is the byte i + 1 before + * scr_arr_states_end(a). Removing slots from the front (head room: `data` + * advances, `cap` drops) leaves every remaining state at its address. The + * address is computed on integers, so a NULL `data` stays NULL. Arrays + * without states have no bytes there. */ static inline uint8_t *scr_arr_states_end(const ScrArr *a) { return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (scr_arr_slot_width(a) + 1)); } @@ -233,6 +250,26 @@ static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t co return scr_arr_states_end(a) - from - count; } +/* The state of dense slot `index` (< cap), with or without state bytes. */ +static inline uint8_t scr_arr_dense_state(const ScrArr *a, size_t index) { + if (!scr_arr_has_states(a)) return index < a->len ? SCR_ARR_VALUE : SCR_ARR_HOLE; + return *scr_arr_state_ptr(a, index); +} + +/* The dense states of `a` for a scan (scr_arr_state_in), or NULL when it + * has none: every index below its length is then a value. */ +static inline const uint8_t *scr_arr_scan_states(const ScrArr *a) { + return scr_arr_has_states(a) ? scr_arr_states_end(a) : NULL; +} + +/* Give `a` state bytes: present values below the length, holes above it. + * May move the dense slots. */ +void scr_arr_add_states(ScrArr *a); + +static inline void scr_arr_need_states(ScrArr *a) { + if (!scr_arr_has_states(a)) scr_arr_add_states(a); +} + /* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; @@ -270,7 +307,8 @@ static inline size_t scr_arr_value_run(const uint8_t *states_end, size_t f, size static inline void scr_arr_refill(ScrArr *a) { size_t end = a->len < a->cap ? a->len : a->cap; size_t f = a->filled; - if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); + if (!scr_arr_has_states(a)) f = end; + else if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), f, end); a->filled = (uint32_t)f; } @@ -302,8 +340,9 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap); ScrArr *scr_arr_new_small(ScrElemKind elem); ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); /* scr_arr_new(elem, cap) for a known final length: the `cap` slots share - * the array's own block (one allocation). Returns +1. */ -ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap); + * the array's own block (one allocation). With `states`, the array starts + * with states (for holes or present undefined). Returns +1. */ +ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states); static inline ScrArr *scr_arr_retain(ScrArr *a) { if (a->rc != SIZE_MAX) { diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7a6a76cf9..7fec7bd07 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3122,7 +3122,7 @@ static void scr_bytes_fill_from_arr(uint8_t *out, ScrBytesElem elem, const ScrAr /* Sparse tails and explicit undefined retain ordinary iteration * semantics. Numeric array slots own no conversion callbacks. */ if (index < arr->cap) { - if (*scr_arr_state_ptr(arr, index) == SCR_ARR_VALUE) + if (scr_arr_dense_state(arr, index) == SCR_ARR_VALUE) memcpy(&values[i], &arr->data[index], sizeof(double)); else values[i] = NAN; } else values[i] = scr_arr_get_number(arr, (double)index); diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index df4da1738..125685dc6 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -25,10 +25,19 @@ static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { ScrArr *out = a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); - out->len = cap; return out; } +/* Copy [from, from + count) of `src` to the end of `out` and raise its + * length over the copy. */ +static void copying_arr_append_range(ScrArr *out, const ScrArr *src, size_t from, size_t count, + bool reverse) { + size_t at = out->len; + scr_arr_copy_range_ex(out, at, src, from, count, reverse, true); + out->len = (uint32_t)(at + count); + scr_arr_refill(out); +} + static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { if (!copying_elem_is_ref(a->elem)) return slot; void *p = copying_slot_to_ptr(slot); @@ -41,7 +50,7 @@ static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) { ScrArr *scr_arr_to_reversed(const ScrArr *a) { ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, true, true); + copying_arr_append_range(out, a, 0, a->len, true); return out; } @@ -60,10 +69,9 @@ ScrArr *scr_arr_to_spliced(const ScrArr *a, double start, } size_t out_len = a->len - ndelete + items->len; ScrArr *out = copying_arr_new_like(a, out_len); - scr_arr_copy_range_ex(out, 0, a, 0, from, false, true); - scr_arr_copy_range_ex(out, from, items, 0, items->len, false, true); - scr_arr_copy_range_ex(out, from + items->len, a, from + ndelete, - a->len - from - ndelete, false, true); + copying_arr_append_range(out, a, 0, from, false); + copying_arr_append_range(out, items, 0, items->len, false); + copying_arr_append_range(out, a, from + ndelete, a->len - from - ndelete, false); return out; } @@ -88,7 +96,7 @@ ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_f64(out, (double)i, value); return out; } @@ -97,7 +105,7 @@ ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_bool(out, (double)i, value); return out; } @@ -106,7 +114,7 @@ ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); uint64_t retained = copying_arr_retain_slot(a, copying_slot_from_ptr(value)); scr_arr_set_ref(out, (double)i, copying_slot_to_ptr(retained)); @@ -117,7 +125,7 @@ ScrArr *scr_arr_with_undefined(ScrArr *a, double index) { size_t i; if (!copying_arr_with_index(a, index, &i)) return NULL; ScrArr *out = copying_arr_new_like(a, a->len); - scr_arr_copy_range_ex(out, 0, a, 0, a->len, false, true); + copying_arr_append_range(out, a, 0, a->len, false); scr_arr_set_undefined(out, (double)i); return out; } diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 5e5029e14..5956d5dd2 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -1244,9 +1244,9 @@ void scr_cyc_on_release(void *obj) { #define SCR_CYC_ARRAY_WALK(name, visit) \ static __attribute__((noinline)) void name(const ScrArr *arr, void *ctx) { \ size_t end = arr->len < arr->cap ? arr->len : arr->cap; \ - size_t filled = arr->filled < end ? arr->filled : end; \ + const uint8_t *states = scr_arr_scan_states(arr); \ + size_t filled = !states || arr->filled > end ? end : arr->filled; \ for (size_t i = 0; i < filled; i++) visit((void *)(uintptr_t)arr->data[i], ctx); \ - const uint8_t *states = scr_arr_states_end(arr); \ for (size_t i = filled; i < end; i++) \ if (*scr_arr_state_in(states, i) == SCR_ARR_VALUE) \ visit((void *)(uintptr_t)arr->data[i], ctx); \ diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c index a333aab95..25a074f49 100644 --- a/packages/runtime/src/scr_itable.c +++ b/packages/runtime/src/scr_itable.c @@ -472,7 +472,7 @@ static ScrArrState scr_itable_element(ScrITableElements *w, size_t i, uint64_t * } if (i < a->cap) { *slot = a->data[i]; - return (ScrArrState)*scr_arr_state_ptr(a, i); + return (ScrArrState)scr_arr_dense_state(a, i); } if (scr_arr_ext_of(a)) { while (w->sparse < scr_arr_ext_of(a)->sparse_len && scr_arr_ext_of(a)->sparse[w->sparse].index < i) w->sparse++; diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c index f6d98654a..c85370add 100644 --- a/packages/runtime/src/scr_itable.test.c +++ b/packages/runtime/src/scr_itable.test.c @@ -55,6 +55,7 @@ static ScrArr *make_f64s(const double *values, size_t n) { a->filled = (uint32_t)n; a->elem = SCR_ELEM_F64; /* Values, then one state byte per slot (scr_arr_states_end). */ + a->spare = SCR_ARR_STATES; a->data = calloc(n ? n : 1, sizeof(uint64_t) + 1); for (size_t i = 0; i < n; i++) memcpy(&a->data[i], &values[i], sizeof(double)); memset(scr_arr_state_run(a, 0, n), SCR_ARR_VALUE, n); @@ -170,6 +171,7 @@ int main(void) { assert(scr_itable_size(lists) == 4); ScrArr *big = make_f64s(NULL, 0); big->len = big->cap = big->filled = 100; + big->spare = 0; /* values without states */ free(big->data); big->data = calloc(100, sizeof(uint64_t)); for (unsigned i = 0; i < 100; i++) memcpy(&big->data[i], &(double){i}, sizeof(double)); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 4be596ab5..3f7dcac45 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12961,50 +12961,50 @@ static bool scr_set_to_arr_sparse(const ScrMap *s, ScrArr *out) { ScrArr *scr_set_to_arr_f64(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_F64, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_bool(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_BOOL, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; - scr_arr_dense_put(out, out->len, scr_map_entry_at(s, i)->key != 0); - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + scr_arr_dense_put(out, out->len++, scr_map_entry_at(s, i)->key != 0); } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_str(const ScrMap *s) { ScrArr *out = scr_arr_new(SCR_ELEM_STR, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; scr_str_retain((ScrStr *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { ScrArr *out = scr_arr_new_ops(ops, s->nlive); if (scr_set_to_arr_sparse(s, out)) return out; - uint8_t *states = scr_arr_states_end(out); + /* A fresh array without states: the values in order. */ for (size_t i = 0; i < s->nentries; i++) { if (!scr_map_entry_live(s, i)) continue; ops->retain((void *)(uintptr_t)scr_map_entry_at(s, i)->key); - out->data[out->len] = scr_map_entry_at(s, i)->key; - *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; + out->data[out->len++] = scr_map_entry_at(s, i)->key; } + scr_arr_refill(out); return out; } diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 99caa31a8..1d0cf64b6 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1600,7 +1600,7 @@ void scr_set_add_all(ScrMap *set, ScrArr *values) { /* Packed values stay owned by the seed array throughout construction. * Avoid taking a temporary owner only to release it after insertion. * Sparse/missing positions retain the existing accessor semantics. */ - if (i < values->cap && *scr_arr_state_ptr(values, i) == SCR_ARR_VALUE) { + if (i < values->cap && scr_arr_dense_state(values, i) == SCR_ARR_VALUE) { uint64_t key = scr_arr_dense_get(values, i); if (set->key_kind == SCR_MAP_KEY_F64) key = scr_map_f64_bits(scr_map_slot_to_f64(key)); scr_map_set(set, scr_map_hash_key(set, key), key, 0); diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 675a13ca9..3cb6c5b55 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -209,7 +209,7 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, a->data[i], fn); if (scr_arr_ext_of(a)) diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index b62cba896..158697a14 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -692,7 +692,7 @@ static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { for (int k = 1; k < capture_count; k++) { size_t length = strlen(name); bool matched = (size_t)k < a->len && (size_t)k < a->cap && - *scr_arr_state_ptr(a, (size_t)k) == SCR_ARR_VALUE; + scr_arr_dense_state(a, (size_t)k) == SCR_ARR_VALUE; /* ES2025 duplicates sit in distinct alternatives: at most one * participates, and it wins over an earlier undefined. */ if (length && (matched || !scr_dyn_obj_get(groups, name, length))) @@ -845,7 +845,7 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool if (rc == 1) { int count = lre_get_capture_count(bc); bool spans = lazy != 0 && !input.wide && (count >= 64 || ((mask | lazy) >> count) == 0); - out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans); + out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans, true); /* A fresh dense array: every slot is written directly, then the * hole-free prefix is the run of materialized captures from 0. Its * storage pointers are fixed (the inline block never moves here), so @@ -905,14 +905,15 @@ static bool scr_regex_group_bytes(const ScrArr *m, double k, const char **p, siz /* Array lengths stay below 2^53, so signed conversions are exact. */ if (!(k >= 0 && k < (double)(int64_t)m->len)) return false; size_t i = (size_t)(int64_t)k; - if (*scr_arr_state_ptr(m, i) == SCR_ARR_VALUE) { + uint8_t state = scr_arr_dense_state(m, i); + if (state == SCR_ARR_VALUE) { const ScrStr *str = (const ScrStr *)(uintptr_t)m->data[i]; *p = str->data; *n = str->len; return true; } uint64_t span = m->data[i]; - if (*scr_arr_state_ptr(m, i) != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; + if (state != SCR_ARR_UNDEFINED || span == SCR_RE_NO_SPAN) return false; const ScrStr *subject = (const ScrStr *)(uintptr_t)m->data[m->len - 1]; *p = subject->data + (span >> 32); *n = (size_t)((span & UINT32_MAX) - (span >> 32)); @@ -1043,10 +1044,13 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices int start = scr_capture_index(&input, capture[0]); int end = scr_capture_index(&input, capture[1]); /* One allocation per row, like an exec result: every slot is written - * directly, then the hole-free prefix is the run of captures from 0. */ - ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count); + * directly, then the hole-free prefix is the run of captures from 0. + * A row needs states only for a capture that did not participate. */ + bool all = true; + for (int k = 0; k < capture_count; k++) all &= capture[2 * k] != NULL && capture[2 * k + 1] != NULL; + ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count, !all); uint64_t *data = row->data; - uint8_t *states = scr_arr_states_end_w(row, 8); + uint8_t *states = all ? NULL : scr_arr_states_end_w(row, 8); for (int k = 0; k < capture_count; k++) { const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1]; if (cs == NULL || ce == NULL) { @@ -1056,11 +1060,11 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices ScrStr *text = scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)); data[k] = (uint64_t)(uintptr_t)text; - *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; + if (states) *scr_arr_state_in(states, (size_t)k) = SCR_ARR_VALUE; } } size_t filled = 0; - while (filled < (size_t)capture_count && *scr_arr_state_in(states, filled) == SCR_ARR_VALUE) + while (filled < (size_t)capture_count && (!states || *scr_arr_state_in(states, filled) == SCR_ARR_VALUE)) filled++; row->filled = (uint32_t)filled; row->len = (size_t)capture_count; @@ -2795,7 +2799,7 @@ static ScrDyn *scr_native_regex_invoke(void *ptr, ScrDyn *self, const char *meth if (!row) return scr_exc_pending() ? NULL : scr_dyn_new_null(); ScrDyn *match = scr_dyn_new_arr(); for (size_t k = 0; k < row->len; k++) - scr_dyn_arr_push(match, *scr_arr_state_ptr(row, k) == SCR_ARR_VALUE + scr_dyn_arr_push(match, scr_arr_dense_state(row, k) == SCR_ARR_VALUE ? scr_dyn_new_str((ScrStr *)(uintptr_t)row->data[k]) : scr_dyn_retain(scr_dyn_undefined())); scr_arr_copy_metadata(row, match); diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index dbd74f3ac..25bbcf7ae 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -805,7 +805,7 @@ static void scr_rgn_walk_arr(ScrRgnWalk *w, ScrArr *a) { ScrRgnSlotKind k = a->elem == SCR_ELEM_ARR ? (ScrRgnSlotKind){SCR_RGN_W_ARR, NULL} : scr_rgn_slot_kind(scr_arr_ops(a) ? scr_arr_ops(a)->release : NULL); size_t dense = a->len < a->cap ? a->len : a->cap; - const uint8_t *states = scr_arr_states_end(a); + const uint8_t *states = scr_arr_scan_states(a); for (size_t i = 0; i < dense; i++) if (!states || *scr_arr_state_in(states, i) == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->data[i]), k); if (!scr_arr_ext_of(a)) return; diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 58272cf67..f94554642 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -2571,15 +2571,11 @@ ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit_num) { while (count < 64 && (bytes = scr_str_split_span(s, sep, &cursor, false, &len))) slots[count++] = (uint64_t)(uintptr_t)(s->ascii ? scr_str_from_span_ascii(bytes, len) : scr_str_from_span(bytes, len)); - ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count); + ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count, false); if (count) { - /* A fresh inline array of exactly `count` slots: store the pieces and - * their states directly. */ - uint8_t *states = scr_arr_states_end(out); - for (size_t k = 0; k < count; k++) { - out->data[k] = slots[k]; - *scr_arr_state_in(states, k) = SCR_ARR_VALUE; - } + /* A fresh inline array of exactly `count` slots: store the pieces + * directly. */ + for (size_t k = 0; k < count; k++) out->data[k] = slots[k]; out->len = (uint32_t)count; out->filled = (uint32_t)count; } diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index d943acfb2..6e4ead89a 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -1365,11 +1365,13 @@ static void test_exact_construction(void) { } /* The hole-free prefix bound never covers a non-value state, and the - * operations that keep an array hole-free keep the bound at its length. */ + * operations that keep an array hole-free keep the bound at its length. An + * array without states keeps its elements in dense storage. */ static bool filled_ok(const ScrArr *a) { if (a->filled > a->len || a->filled > a->cap) return false; + if (!scr_arr_has_states(a) && (a->len > a->cap || a->filled != a->len)) return false; for (size_t i = 0; i < a->filled; i++) - if (*scr_arr_state_ptr(a, i) != SCR_ARR_VALUE) return false; + if (scr_arr_dense_state(a, i) != SCR_ARR_VALUE) return false; return true; } @@ -1444,7 +1446,7 @@ static void test_filled_prefix(void) { scr_arr_release(scr_arr_sort_primitive(b, false)); check(filled_ok(b), "sorting moves holes past the bound"); for (size_t i = 0; i < b->len; i++) { - if (i < b->cap && *scr_arr_state_ptr(b, i) != SCR_ARR_VALUE) break; + if (i < b->cap && scr_arr_dense_state(b, i) != SCR_ARR_VALUE) break; check(i < b->filled, "sorted values form the bound"); } while (scr_arr_len(b) > 0) { @@ -1464,11 +1466,13 @@ static void test_filled_prefix(void) { } /* Boolean arrays store one byte per dense element, followed by the states in - * the same allocation. Growth moves the states to the end of the larger area, - * densifying absorbs sparse entries into bytes, and every moving and copying - * operation keeps values, holes and present undefined in place. */ + * the same allocation once they have states. Growth moves the states to the + * end of the larger area, densifying absorbs sparse entries into bytes, and + * every moving and copying operation keeps values, holes and present + * undefined in place. */ static bool bool_layout_ok(const ScrArr *a) { - return a->cap == 0 || scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; + return a->cap == 0 || !scr_arr_has_states(a) || + scr_arr_states_end(a) == (const uint8_t *)a->data + 2 * (size_t)a->cap; } static bool bool_states(const ScrArr *a, const char *want) { @@ -1584,7 +1588,7 @@ static void test_inline_slots(void) { * block; growth past them moves them out like any inline array. */ ScrArr *known = scr_arr_new(SCR_ELEM_F64, 4); check(inline_slots(known) && known->cap >= 4 && known->len == 0 && !scr_arr_has(known, 0) && - scr_arr_states_end(known) == (uint8_t *)(known->data + known->cap) + known->cap, + !scr_arr_has_states(known), "a known-count construction places its slots in the array block"); size_t known_cap = known->cap; for (size_t i = 0; i <= known_cap; i++) scr_arr_push_f64(known, (double)i); @@ -1605,26 +1609,25 @@ static void test_inline_slots(void) { "known-count boolean constructions are inline too"); scr_arr_release(known_flags); ScrArr *a = scr_arr_new_small(SCR_ELEM_F64); - check(inline_slots(a) && a->cap >= 4 && a->len == 0 && - scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, - "small construction places slots and states in the array block"); + check(inline_slots(a) && a->cap >= 4 && a->len == 0 && !scr_arr_has_states(a), + "small construction places its slots in the array block, without states"); size_t inline_cap = a->cap; for (size_t i = 0; i < inline_cap; i++) scr_arr_push_f64(a, (double)i); check(inline_slots(a) && a->filled == inline_cap, "appends within the inline capacity stay inline"); scr_arr_push_f64(a, 100); check(!inline_slots(a) && a->cap > inline_cap && scr_arr_get_f64(a, 0) == 0 && scr_arr_get_f64(a, (double)inline_cap - 1) == (double)inline_cap - 1 && - scr_arr_get_f64(a, (double)inline_cap) == 100 && - scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap, + scr_arr_get_f64(a, (double)inline_cap) == 100 && !scr_arr_has_states(a), "growth moves inline slots to their own allocation"); scr_arr_release(a); ScrArr *flags = scr_arr_new_small(SCR_ELEM_BOOL); - check(inline_slots(flags) && scr_arr_states_end(flags) == (uint8_t *)flags->data + 2 * (size_t)flags->cap, + check(inline_slots(flags) && flags->cap >= 4 && !scr_arr_has_states(flags), "boolean inline slots are one byte each"); scr_arr_push_bool(flags, true); scr_arr_set_bool(flags, 3, true); - check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3), + check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3) && + scr_arr_has_states(flags) && bool_layout_ok(flags), "boolean inline slots keep holes"); scr_arr_release(flags); @@ -2107,8 +2110,8 @@ static void test_head_room(void) { check_f64(scr_arr_shift_f64(q), 0, "shift answers the first element"); check(q->data == block + 1 && q->cap == cap - 1 && q->len == 63, "shift advances the storage instead of moving the tail"); - check(scr_arr_states_end(q) == (uint8_t *)(block + cap) + cap - 1 && head_room(q) && q->data[-1] == 1, - "the remaining states stay at the end of the allocation"); + check(!scr_arr_has_states(q) && head_room(q) && q->data[-1] == 1, + "shift records the vacated slot in front of the data"); check(q->filled == 63, "shift keeps the hole-free prefix"); for (int i = 1; i < 40; i++) scr_arr_shift_f64(q); check(q->data == block + 40 && q->len == 24 && scr_arr_get_f64(q, 0) == 40 && @@ -2223,6 +2226,159 @@ static void test_head_room(void) { #endif } +/* SCR_ARR_STATES (scr_array.h): an array starts without state bytes and + * keeps none while every index below its length is a present value. A + * hole, a present undefined or a length past the elements gives it states, + * with the elements in place. */ +static ScrStr *str_of(const char *text) { return scr_str_new(text, strlen(text)); } + +static bool strs_are(ScrArr *a, const char *want) { + /* want: one character per element, '_' a hole, 'u' undefined. */ + if (scr_arr_len(a) != (double)strlen(want)) return false; + for (size_t i = 0; want[i]; i++) { + double state = scr_arr_state(a, (double)i); + if (want[i] == '_') { + if (state != SCR_ARR_HOLE) return false; + } else if (want[i] == 'u') { + if (state != SCR_ARR_UNDEFINED) return false; + } else { + if (state != SCR_ARR_VALUE) return false; + ScrStr *got = scr_arr_get_ref(a, (double)i); + bool same = got->len == 1 && got->data[0] == want[i]; + scr_str_release(got); + if (!same) return false; + } + } + return filled_ok(a); +} + +static ScrArr *strs_of(const char *text, bool small) { + ScrArr *a = small ? scr_arr_new_small(SCR_ELEM_STR) : scr_arr_new(SCR_ELEM_STR, 0); + for (size_t i = 0; text[i]; i++) scr_arr_push_ref(a, scr_str_new(text + i, 1)); + return a; +} + +static void test_states_on_demand(void) { +#ifdef SCR_RC_AUDIT + long strings0 = scr_str_live_count(); + long arrays0 = scr_arr_live_count(); +#endif + ScrArr *a = strs_of("abcdef", false); + scr_arr_set_ref(a, 1, str_of("B")); + scr_arr_set_ref(a, 6, str_of("g")); + scr_str_release(scr_arr_pop_ref(a)); + scr_arr_set_len(a, 5); + check(!scr_arr_has_states(a) && a->filled == 5 && strs_are(a, "aBcde"), + "pushes, writes below the length, appends, pops and truncation keep no states"); + scr_arr_set_undefined(a, 2); + check(scr_arr_has_states(a) && strs_are(a, "aBude"), "a present undefined gives the array states"); + scr_arr_release(a); + + ScrArr *gap = strs_of("ab", true); + scr_arr_set_ref(gap, 3, str_of("d")); + check(scr_arr_has_states(gap) && strs_are(gap, "ab_d"), "a write past the length leaves holes"); + scr_arr_release(gap); + + ScrArr *longer = strs_of("abc", true); + scr_arr_set_len(longer, 5); + check(scr_arr_has_states(longer) && strs_are(longer, "abc__"), "a longer length adds holes"); + scr_arr_release(longer); + + /* `new Array(n)` and a length set on an empty small array: the inline + * slots give way to one allocation holding the slots and their states. */ + ScrArr *sized = scr_arr_new_small(SCR_ELEM_STR); + scr_arr_set_len(sized, 6); + scr_arr_set_ref(sized, 4, str_of("e")); + check(scr_arr_has_states(sized) && !(sized->spare & SCR_ARR_INLINE_SLOTS) && sized->cap == 6 && + strs_are(sized, "____e_") && filled_ok(sized), + "a length set on an empty inline array reserves that many slots with states"); + scr_arr_release(sized); + + ScrArr *deleted = strs_of("abcd", false); + scr_arr_delete(deleted, 1); + check(scr_arr_has_states(deleted) && strs_are(deleted, "a_cd"), "delete leaves a hole"); + scr_arr_release(deleted); + + /* Conversion keeps out-of-line storage and head room's elements. */ + ScrArr *props = strs_of("abc", true); + scr_arr_set_ref(props, -1, str_of("p")); + scr_arr_set_undefined(props, 3); + ScrStr *prop = scr_arr_get_ref(props, -1); + check(scr_arr_has_states(props) && strs_are(props, "abcu") && prop->data[0] == 'p', + "states keep a noncanonical property"); + scr_str_release(prop); + scr_arr_release(props); + ScrArr *q = strs_of("abcdefghijklmnopqrst", false); + for (int i = 0; i < 3; i++) scr_str_release(scr_arr_shift_ref(q)); + check(!scr_arr_has_states(q) && head_room(q), "shift keeps no states"); + scr_arr_set_undefined(q, 1); + check(scr_arr_has_states(q) && !head_room(q) && strs_are(q, "dufghijklmnopqrst"), + "states move the elements of head room to the front"); + scr_arr_release(q); + + /* Copies keep no states for values, and states for holes. */ + ScrArr *src = strs_of("abcde", false); + ScrArr *part = scr_arr_slice(src, 1, 4); + ScrArr *joined = scr_arr_slice(src, 0, INFINITY); + scr_arr_concat_copy(joined, src); + ScrArr *spliced = scr_arr_splice(joined, 2, 3); + ScrArr *reversed = scr_arr_to_reversed(src); + scr_arr_release(scr_arr_reverse(src)); + scr_arr_release(scr_arr_sort_primitive(src, false)); + check(!scr_arr_has_states(part) && strs_are(part, "bcd"), "a slice of values keeps no states"); + check(!scr_arr_has_states(joined) && strs_are(joined, "ababcde"), + "concat and splice of values keep no states"); + check(!scr_arr_has_states(spliced) && strs_are(spliced, "cde"), "removed values keep no states"); + check(!scr_arr_has_states(reversed) && strs_are(reversed, "edcba"), "toReversed of values keeps no states"); + check(!scr_arr_has_states(src) && strs_are(src, "abcde"), "reverse and sort of values keep no states"); + scr_arr_release(src); + scr_arr_release(part); + scr_arr_release(joined); + scr_arr_release(spliced); + scr_arr_release(reversed); + ScrArr *holey = strs_of("ab", false); + scr_arr_set_ref(holey, 3, str_of("d")); + ScrArr *spread = strs_of("x", true); + scr_arr_push_spread(spread, holey); + ScrArr *concat = strs_of("x", true); + scr_arr_concat_copy(concat, holey); + ScrArr *values = scr_arr_slice(holey, 0, 2); + ScrArr *front = strs_of("yz", false); + scr_arr_unshift_spread(front, holey); + check(scr_arr_has_states(spread) && strs_are(spread, "xabud") && scr_arr_has_states(concat) && + strs_are(concat, "xab_d") && !scr_arr_has_states(values) && strs_are(values, "ab") && + scr_arr_has_states(front) && strs_are(front, "abudyz"), + "copies of holes keep holes or present undefined"); + scr_arr_release(holey); + scr_arr_release(spread); + scr_arr_release(concat); + scr_arr_release(values); + scr_arr_release(front); + + /* Numbers and booleans take the same paths without references. */ + ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 3); + for (int i = 0; i < 40; i++) scr_arr_push_f64(nums, i); + scr_arr_release(scr_arr_fill_f64(nums, 7, 30, INFINITY)); + check(!scr_arr_has_states(nums) && scr_arr_get_number(nums, 39) == 7 && scr_arr_get_number(nums, 40) != 0, + "number fills keep no states"); + scr_arr_release(scr_arr_fill_undefined(nums, 0, 1)); + check(scr_arr_has_states(nums) && scr_arr_state(nums, 0) == SCR_ARR_UNDEFINED && + scr_arr_get_number(nums, 1) == 1 && scr_arr_get_number(nums, 39) == 7 && filled_ok(nums), + "an undefined fill gives a number array states"); + scr_arr_release(nums); + ScrArr *bits = scr_arr_new_small(SCR_ELEM_BOOL); + for (int i = 0; i < 6; i++) scr_arr_push_bool(bits, i % 2 == 0); + scr_arr_set_undefined(bits, 6); + check(scr_arr_has_states(bits) && bool_layout_ok(bits) && scr_arr_get_bool(bits, 4) && + !scr_arr_get_bool(bits, 5) && scr_arr_state(bits, 6) == SCR_ARR_UNDEFINED, + "boolean arrays get one state byte per element"); + scr_arr_release(bits); +#ifdef SCR_RC_AUDIT + check(scr_str_live_count() == strings0, "states on demand: no strings leaked"); + check(scr_arr_live_count() == arrays0, "states on demand: no arrays leaked"); +#endif +} + int main(int argc, char **argv) { if (argc > 1) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); @@ -2279,6 +2435,7 @@ int main(int argc, char **argv) { test_bool_bytes(); test_inline_slots(); test_head_room(); + test_states_on_demand(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed == 0 ? 0 : 1; From f9bed65a65f732c539f50e32e2ac7db10828e71c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:45:10 -0500 Subject: [PATCH 018/169] Name the union destructuring fence without printing the union The fence for a field the arms of a union type differently printed the whole union, whose arm order follows the order record shapes are interned in and changes with unrelated compiler changes. It now names the union without its arms, as the other union fences do. --- packages/compiler/src/frontend/lowering/lower-stmts.ts | 8 ++------ tests/harness/__snapshots__/destructuring-fences.ts.txt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 831fec83d..67706a285 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3653,11 +3653,7 @@ function lowerUnionObjectPattern( for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; if (el.dotDotDotToken) { - lowerer.unsupported( - "SC1031", - el, - `rest bindings over '${lowerer.fmt(srcType)}' sources (${NARROW_FIRST})`, - ); + lowerer.unsupported("SC1031", el, `rest bindings over union sources (${NARROW_FIRST})`); } const prop = el.propertyName ?? el.name; const propName = @@ -3692,7 +3688,7 @@ function lowerUnionObjectPattern( lowerer.unsupported( "SC1031", el, - `destructuring '${propName}' from a '${lowerer.fmt(srcType)}' source (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`, + `destructuring '${propName}' from a union whose arms type it differently (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`, ); } let value: IrExpr = { diff --git a/tests/harness/__snapshots__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 2f6afe734..41026a7c8 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -19,7 +19,7 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a | ^~~~~~ 32 | console.log(onlyIn); -destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a '{ b: string; count: number; kind: string } | { b: number; kind: string; label: string }' source (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet +destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a union whose arms type it differently (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet 44 | function fromUnion(u: E1 | E2): string { 45 | const { b } = u; // union source whose arms type the field differently: narrow first From 17b70e6d6a02ca09bdb6f9214e73305731a03410 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:57:59 -0500 Subject: [PATCH 019/169] Find Buffer search candidates with memchr --- .../7249-buffer-index-of-candidates.ts.json | 6 +++++ packages/runtime/src/scr_bytes.c | 14 +++++++--- .../corpus/7249-buffer-index-of-candidates.ts | 27 +++++++++++++++++++ .../7249-buffer-index-of-candidates.ts.json | 1 + 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json create mode 100644 tests/corpus/7249-buffer-index-of-candidates.ts create mode 100644 tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json new file mode 100644 index 000000000..f5299cb9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7249-buffer-index-of-candidates.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7a6a76cf9..183174555 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3271,16 +3271,24 @@ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, } if (nlen == 0) return o > (double)len ? (double)len : o; /* '' matches at the clamp */ if (nlen > len) return -1; + /* Candidates are the positions holding the needle's first byte; only + * those compare the rest. */ + const uint8_t first = needle->data[0]; if (fwd) { size_t start = o > (double)len ? len : (size_t)o; - for (size_t i = start; i + nlen <= len; i++) { - if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i; + const uint8_t *at = b->data + start, *end = b->data + (len - nlen) + 1; + while (at < end) { + at = memchr(at, first, (size_t)(end - at)); + if (!at) return -1; + if (nlen == 1 || memcmp(at + 1, needle->data + 1, nlen - 1) == 0) return (double)(at - b->data); + at++; } return -1; } size_t start = o > (double)(len - nlen) ? len - nlen : (size_t)o; for (size_t i = start;; i--) { - if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i; + if (b->data[i] == first && (nlen == 1 || memcmp(b->data + i + 1, needle->data + 1, nlen - 1) == 0)) + return (double)i; if (i == 0) break; } return -1; diff --git a/tests/corpus/7249-buffer-index-of-candidates.ts b/tests/corpus/7249-buffer-index-of-candidates.ts new file mode 100644 index 000000000..ae6c2ae23 --- /dev/null +++ b/tests/corpus/7249-buffer-index-of-candidates.ts @@ -0,0 +1,27 @@ +// Buffer indexOf, lastIndexOf and includes with needles whose first byte +// recurs: a candidate whose later bytes differ is skipped, matches at the +// first and last positions are found, byteOffset clamps and counts from the +// end, and the search reads only the buffer's own window of its memory. +const text = Buffer.from("aab aaab aaaab ab\nab"); +console.log(text.indexOf("ab"), text.indexOf("aab"), text.indexOf("aaab"), text.indexOf("aaaab"), text.indexOf("aaaaab")); +console.log(text.lastIndexOf("ab"), text.lastIndexOf("aab"), text.lastIndexOf("aaab"), text.lastIndexOf("b\na")); +console.log(text.indexOf("ab", 2), text.indexOf("ab", -3), text.indexOf("ab", 100), text.indexOf("ab", -100)); +console.log(text.lastIndexOf("ab", 5), text.lastIndexOf("ab", -4), text.lastIndexOf("ab", 0), text.lastIndexOf("aa", 1)); +console.log(text.indexOf(10), text.indexOf(97, 3), text.lastIndexOf(98), text.lastIndexOf(97, -2), text.includes("b a")); +console.log(text.indexOf(""), text.indexOf("", 4), text.indexOf("", 100), text.lastIndexOf(""), text.indexOf("x")); +console.log(text.indexOf(Buffer.from("a\nab")), text.indexOf("ab\nab"), text.indexOf("ab\nabc"), text.lastIndexOf("aab", 4)); + +// A window into a larger buffer never sees the bytes outside it. +const whole = Buffer.from("xxneedlexx needle needlexx"); +const window = whole.subarray(3, 22); +console.log(window.toString(), window.indexOf("needle"), window.lastIndexOf("needle"), window.indexOf("xx"), window.includes("xne")); +console.log(window.indexOf("eedle", 1), window.lastIndexOf("eed"), window.indexOf("needlex"), window.lastIndexOf("n", 0)); + +// A long search with many first-byte candidates. +const long = Buffer.alloc(20000, "ab"); +long.write("abc", 19990); +console.log(long.indexOf("abc"), long.lastIndexOf("aba"), long.indexOf("abc", 19991), long.lastIndexOf("abc"), long.indexOf("bb")); +const lines: number[] = []; +const fasta = Buffer.from(">ONE\nACGT\nTTGA\n>TWO\nGGCC\n"); +for (let at = fasta.indexOf(10); at !== -1; at = fasta.indexOf(10, at + 1)) lines.push(at); +console.log(lines.join(","), fasta.indexOf(62, 1), fasta.lastIndexOf(62)); diff --git a/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json new file mode 100644 index 000000000..5c265ea01 --- /dev/null +++ b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1 @@ +{ "plain": 2870, "san": 3656 } From 1e0dabd74864c4c2f4e6c962cf550bad540047a6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:05:46 -0500 Subject: [PATCH 020/169] Declare every http2 listener event for once and addListener The lowering accepts on, once and addListener alike for each server, session and stream kind, but the fallback Node types (used when a project has no @types/node) declared fewer events for once than for on on the http2 servers, sessions and streams, and no addListener on any of them. `http2Server.once("connection", ...)` and `server.addListener("stream", ...)` failed to type-check (SC0001) where Node runs them. once and addListener on Http2Server, Http2SecureServer, Http2Session, Http2Stream, ServerHttp2Stream and ClientHttp2Stream now declare the same overloads as on. Corpus 7281 runs an HTTP CONNECT proxy on node:http, which #1791's declaration of the http.Server 'connect' event made compile: it tunnels to a local target and pipes both ways, with a non-empty head, once and addListener registrations, and an 'upgrade' handover. Corpus 7282 registers http2 server, session and stream listeners with once and addListener. Closes #1439 --- .../ambient/scriptc-node-fallback.d.ts | 142 +++++++++++++++++- .../7281-http-server-connect-tunnel.ts.json | 6 + .../7282-http2-once-add-listener.ts.json | 6 + .../corpus/7281-http-server-connect-tunnel.ts | 81 ++++++++++ tests/corpus/7282-http2-once-add-listener.ts | 54 +++++++ .../7281-http-server-connect-tunnel.ts.json | 1 + .../7282-http2-once-add-listener.ts.json | 1 + 7 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json create mode 100644 tests/corpus/7281-http-server-connect-tunnel.ts create mode 100644 tests/corpus/7282-http2-once-add-listener.ts create mode 100644 tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json create mode 100644 tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..02b88a677 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5929,6 +5929,7 @@ declare module "http2" { export interface Http2Stream { on(event: "error" | "close", listener: (err: Error) => void): void; once(event: "error" | "close", listener: (err: Error) => void): void; + addListener(event: "error" | "close", listener: (err: Error) => void): void; destroy(): void; } export interface Http2ServerRequest { @@ -6029,11 +6030,6 @@ declare module "http2" { listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, ): void; on(event: "session", listener: (session: ServerHttp2Session) => void): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once(event: "session", listener: (session: ServerHttp2Session) => void): void; /* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the * allowHTTP1 lowering never sees): the second argument is the raw * socket on HTTP/1.1 connections; narrow with instanceof. */ @@ -6051,6 +6047,42 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close", listener: () => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once(event: "session", listener: (session: ServerHttp2Session) => void): void; + once( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + addListener(event: "connection", listener: (socket: Socket) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close", listener: () => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; } export interface SecureServerOptions { allowHTTP1?: boolean; @@ -6392,6 +6424,28 @@ declare module "http2" { listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, ): void; once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "ping", listener: (payload: Uint8Array) => void): void; + addListener(event: "close" | "timeout", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener(event: "ping", listener: (payload: Uint8Array) => void): void; } export interface ClientHttp2Session extends Http2Session { @@ -6469,7 +6523,30 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; respond( headers?: OutgoingHttpHeaders, options?: { endStream?: boolean; waitForTrailers?: boolean }, @@ -6519,6 +6596,7 @@ declare module "http2" { on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "data", listener: (chunk: any) => void): void; once( event: @@ -6533,6 +6611,42 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener( + event: "response", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "push", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "headers", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; } export interface Http2Server { @@ -6567,6 +6681,24 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close" | "listening" | "timeout", listener: () => void): void; + once(event: "connection", listener: (socket: import("net").Socket) => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close" | "listening" | "timeout", listener: () => void): void; + addListener(event: "connection", listener: (socket: import("net").Socket) => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; } export interface ServerOptions { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 000000000..2328d508b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7281-http-server-connect-tunnel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json new file mode 100644 index 000000000..96c879197 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7282-http2-once-add-listener.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7281-http-server-connect-tunnel.ts b/tests/corpus/7281-http-server-connect-tunnel.ts new file mode 100644 index 000000000..d8b396c07 --- /dev/null +++ b/tests/corpus/7281-http-server-connect-tunnel.ts @@ -0,0 +1,81 @@ +// An HTTP CONNECT proxy on node:http: the server's 'connect' listener gets +// the request, the raw client socket and the bytes read past the request +// head, opens a TCP connection to the requested host and pipes both ways. +// 'once' and 'addListener' register the same event, and 'upgrade' hands +// over the socket for an Upgrade request. +import * as http from "node:http"; +import * as net from "node:net"; + +const target = net.createServer((socket) => { + socket.on("data", (d) => socket.write(`echo:${d.toString()}`)); +}); + +const proxy = http.createServer((_req, res) => res.end("plain")); +const seen: string[] = []; +proxy.on("connect", (req, clientSocket, head) => { + seen.push(`connect ${req.method} ${req.url} head=${head.length}`); + const [host, port] = (req.url ?? "").split(":"); + const upstream = net.connect(Number(port), host, () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + if (head.length > 0) upstream.write(head); + upstream.pipe(clientSocket); + clientSocket.pipe(upstream); + }); +}); +proxy.once("connect", (req) => seen.push(`once ${req.url}`)); +proxy.addListener("upgrade", (req, socket, head) => { + seen.push(`upgrade ${req.headers.upgrade} head=${head.length}`); + socket.end("HTTP/1.1 101 Switching Protocols\r\nUpgrade: echo\r\nConnection: Upgrade\r\n\r\n"); +}); + +function tunnel( + proxyPort: number, + targetPort: number, + payload: string, + withHead: boolean, +): Promise { + return new Promise((resolve) => { + const c = net.connect(proxyPort, "127.0.0.1", () => { + const request = `CONNECT 127.0.0.1:${targetPort} HTTP/1.1\r\nHost: 127.0.0.1:${targetPort}\r\n\r\n`; + c.write(withHead ? request + payload : request); + }); + let text = ""; + let sent = withHead; + c.on("data", (d) => { + text += d.toString(); + if (!sent && text.includes("\r\n\r\n")) { + sent = true; + c.write(payload); + } + if (sent && text.includes(`echo:${payload}`)) { + c.end(); + resolve(text); + } + }); + }); +} + +function upgrade(proxyPort: number): Promise { + return new Promise((resolve) => { + const u = net.connect(proxyPort, "127.0.0.1", () => { + u.write("GET /ws HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: echo\r\n\r\n"); + }); + let text = ""; + u.on("data", (d) => (text += d.toString())); + u.on("end", () => resolve(text.split("\r\n")[0] ?? "")); + }); +} + +target.listen(0, "127.0.0.1", () => { + const targetPort = (target.address() as net.AddressInfo).port; + proxy.listen(0, "127.0.0.1", async () => { + const proxyPort = (proxy.address() as net.AddressInfo).port; + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "first", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "second", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "with-head", true))); + console.log(JSON.stringify(await upgrade(proxyPort))); + console.log(seen.join("\n").replaceAll(String(targetPort), "PORT")); + proxy.close(); + target.close(); + }); +}); diff --git a/tests/corpus/7282-http2-once-add-listener.ts b/tests/corpus/7282-http2-once-add-listener.ts new file mode 100644 index 000000000..ab1e24a9f --- /dev/null +++ b/tests/corpus/7282-http2-once-add-listener.ts @@ -0,0 +1,54 @@ +// node:http2 listeners registered with once and addListener on the server, +// the client session and both stream kinds. A once listener runs for the +// first event only; addListener is on. +import * as http2 from "node:http2"; +import * as net from "node:net"; + +const log: string[] = []; +const accepted: string[] = []; +const server = http2.createServer(); +server.once("connection", (socket: net.Socket) => accepted.push(`connection ${typeof socket.remotePort}`)); +server.addListener("session", () => accepted.push("session")); +server.once("stream", () => log.push("server first stream")); +server.addListener("stream", (stream, headers) => { + const header = headers[":path"]; + const path = typeof header === "string" ? header : ""; + log.push(`server stream ${path}`); + let body = ""; + stream.addListener("data", (chunk) => (body += chunk.toString())); + stream.once("end", () => { + stream.respond({ ":status": 200, "x-path": path }); + stream.end(`${path}:${body}`); + }); +}); +server.once("close", () => log.push("server close")); + +function request(client: http2.ClientHttp2Session, path: string, body: string): Promise { + return new Promise((resolve) => { + const req = client.request({ ":path": path, ":method": "POST" }); + req.once("response", (headers) => { + const status = headers[":status"]; + const echoed = headers["x-path"]; + log.push(`client response ${typeof status === "number" ? status : "?"} ${typeof echoed === "string" ? echoed : "?"}`); + }); + let text = ""; + req.addListener("data", (chunk) => (text += chunk.toString())); + req.once("end", () => resolve(text)); + req.end(body); + }); +} + +server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const client = http2.connect(`http://localhost:${port}`); + client.once("connect", () => log.push("client connect")); + client.addListener("close", () => { + log.push("client close"); + server.close(() => console.log([accepted.sort().join(", "), ...log].join("\n"))); + }); + void (async () => { + console.log(await request(client, "/a", "one")); + console.log(await request(client, "/b", "two")); + client.close(); + })(); +}); diff --git a/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 000000000..c35274f8c --- /dev/null +++ b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3100 } diff --git a/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json new file mode 100644 index 000000000..9e5407833 --- /dev/null +++ b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5100 } From 9aa3b15174dc524eb1157429e133710a9b54a547 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:44:27 -0500 Subject: [PATCH 021/169] Support construct, writev, decodeStrings and defaultEncoding on streams --- .../builtin-apis/streams-and-readline.mdx | 2 + .../limitations/builtin-function-values.mdx | 3 +- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 46 +- packages/compiler/src/backend/llvm/emitter.ts | 7 +- .../src/backend/llvm/expr-stream-callbacks.ts | 76 ++- .../compiler/src/backend/llvm/lib-shared.ts | 6 + .../compiler/src/backend/llvm/lib-stream.ts | 13 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/containers/bytes.ts | 3 +- .../src/frontend/lowering/lower-classes.ts | 13 +- .../src/frontend/lowering/lower-stream.ts | 255 ++++++++-- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 6 + packages/compiler/src/ir/runtime-features.ts | 1 + packages/compiler/src/ir/validate.ts | 7 + .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7246-stream-construct.ts.json | 6 + .../corpus/7247-stream-construct.mjs.json | 6 + .../tests/corpus/7248-stream-writev.ts.json | 6 + .../tests/corpus/7249-stream-writev.cjs.json | 6 + .../corpus/7250-stream-decode-strings.ts.json | 6 + .../7251-stream-write-encodings.mjs.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn_builtin_stream.c | 120 +++-- packages/runtime/src/scr_stream.c | 481 +++++++++++++++--- packages/runtime/src/scr_stream.h | 17 + tests/corpus/7246-stream-construct.ts | 156 ++++++ tests/corpus/7247-stream-construct.mjs | 101 ++++ tests/corpus/7248-stream-writev.ts | 124 +++++ tests/corpus/7249-stream-writev.cjs | 89 ++++ tests/corpus/7250-stream-decode-strings.ts | 89 ++++ tests/corpus/7251-stream-write-encodings.mjs | 65 +++ .../7246-stream-construct.ts.json | 1 + .../7247-stream-construct.mjs.json | 1 + .../corpus-costs/7248-stream-writev.ts.json | 1 + .../corpus-costs/7249-stream-writev.cjs.json | 1 + .../7250-stream-decode-strings.ts.json | 1 + .../7251-stream-write-encodings.mjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 45 files changed, 1543 insertions(+), 206 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json create mode 100644 tests/corpus/7246-stream-construct.ts create mode 100644 tests/corpus/7247-stream-construct.mjs create mode 100644 tests/corpus/7248-stream-writev.ts create mode 100644 tests/corpus/7249-stream-writev.cjs create mode 100644 tests/corpus/7250-stream-decode-strings.ts create mode 100644 tests/corpus/7251-stream-write-encodings.mjs create mode 100644 tests/harness/corpus-costs/7246-stream-construct.ts.json create mode 100644 tests/harness/corpus-costs/7247-stream-construct.mjs.json create mode 100644 tests/harness/corpus-costs/7248-stream-writev.ts.json create mode 100644 tests/harness/corpus-costs/7249-stream-writev.cjs.json create mode 100644 tests/harness/corpus-costs/7250-stream-decode-strings.ts.json create mode 100644 tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 8461cc71c..2a514a5a0 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -23,6 +23,8 @@ `stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. + The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported. + `setDefaultEncoding()` takes any encoding Node knows. A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. `on`, `once`, and `addListener` return the interface, so registrations chain. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index c1bab6680..0fa62e558 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -28,7 +28,8 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it. Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields. Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works. -The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`. +The options the native streams refuse (`signal` and others) are refused here too, as are `_writev` on a Transform, `_final` on a Transform, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, and pipes into a destination that overrides `write()` or `end()`. +`_construct` and `_writev` are read when `Readable.call(this, options)` or `Writable.call(this, options)` runs, as Node's constructors read them. Other class constructors passed to `util.inherits` or `util.deprecate` are refused. Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported. A stored `util.deprecate` reports a `length` of 4, where Node reports 3. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 630ef7173..a7a5e718a 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v26"); + expect(error?.stderr).toContain("scr_runtime_abi_v27"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..abbd023d4 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -7090,7 +7090,11 @@ declare module "stream" { encoding: string, callback: (error?: Error | null) => void, ): void; - writev?(this: Writable, chunks: unknown[], callback: (error?: Error | null) => void): void; + writev?( + this: Writable, + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; final?(this: Writable, callback: (error?: Error | null) => void): void; destroy?(this: Writable, error: Error | null, callback: (error?: Error | null) => void): void; construct?(this: Writable, callback: (error?: Error | null) => void): void; @@ -7125,7 +7129,11 @@ declare module "stream" { readable?: boolean; writable?: boolean; defaultEncoding?: string; - writev?(this: Duplex, chunks: unknown[], callback: (error?: Error | null) => void): void; + writev?( + this: Duplex, + chunks: Array<{ chunk: any; encoding: string }>, + callback: (error?: Error | null) => void, + ): void; signal?: AbortSignal; } interface TransformOptions { @@ -7210,6 +7218,36 @@ declare module "stream" { ): void; } & ObjectModeFlag; + /* decodeStrings: false — a string write reaches write/transform as the + * string with its encoding (Buffers keep 'buffer'). */ + interface StringWritableOptions extends Omit { + decodeStrings: false; + write?( + this: Writable, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + } + type StringDuplexOptions = Omit & { + decodeStrings: false; + write?( + this: Duplex, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null) => void, + ): void; + }; + type StringTransformOptions = Omit & { + decodeStrings: false; + transform?( + this: Transform, + chunk: string | Buffer, + encoding: string, + callback: (error?: Error | null, data?: Buffer | string) => void, + ): void; + }; + /* The operators' options: concurrency and highWaterMark bound map's * in-flight callbacks and queued results. */ interface ArrayOptions { @@ -7340,6 +7378,7 @@ declare module "stream" { class Writable extends EventEmitter { constructor(opts: ObjectWritableOptions); + constructor(opts: StringWritableOptions); constructor(opts?: WritableOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -7418,6 +7457,7 @@ declare module "stream" { * again. The lowering treats both halves as first-class. */ class Duplex extends Readable { constructor(opts: ObjectDuplexOptions); + constructor(opts: StringDuplexOptions); constructor(opts?: DuplexOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -7517,6 +7557,7 @@ declare module "stream" { class Transform extends Duplex { constructor(opts: ObjectTransformOptions); + constructor(opts: StringTransformOptions); constructor(opts?: TransformOptions); /* The underscore-method surface (see Readable). */ _transform( @@ -7528,6 +7569,7 @@ declare module "stream" { } class PassThrough extends Transform { constructor(opts: ObjectTransformOptions); + constructor(opts: StringTransformOptions); constructor(opts?: TransformOptions); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c1cf66af4..4cfa36b5b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -6111,7 +6111,10 @@ export class LlEmitter { return streamDataAdapter(this, cbT); } - streamDoneFnFor(kind: "w" | "f" | "d" | "t" | "l", doneT: IrType & { kind: "func" }): string { + streamDoneFnFor( + kind: "w" | "f" | "d" | "t" | "l" | "c", + doneT: IrType & { kind: "func" }, + ): string { return streamDoneFnFor(this, kind, doneT); } @@ -6135,7 +6138,7 @@ export class LlEmitter { return zlibBytesThunkFor(this, cbT); } - streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string { + streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v", cbT: IrType): string { return streamCbThunkFor(this, kind, cbT); } diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 0570d0ea7..f50ae95e3 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -212,7 +212,7 @@ export function streamDataAdapter( export function streamDoneFnFor( host: LlvmEmitterContext, - kind: "w" | "f" | "d" | "t" | "l", + kind: "w" | "f" | "d" | "t" | "l" | "c", doneT: IrType & { kind: "func" }, ): string { const key = `sd:${kind}:${typeKey(doneT)}`; @@ -256,19 +256,21 @@ export function streamDoneFnFor( } } const entry = - kind === "w" - ? "scr_stream_write_done" - : kind === "f" - ? "scr_stream_final_done" - : kind === "d" - ? "scr_stream_destroy_done" - : kind === "t" - ? valueData - ? "scr_stream_transform_done_value" - : "scr_stream_transform_done" - : valueData - ? "scr_stream_flush_done_value" - : "scr_stream_flush_done"; + kind === "c" + ? "scr_stream_construct_done" + : kind === "w" + ? "scr_stream_write_done" + : kind === "f" + ? "scr_stream_final_done" + : kind === "d" + ? "scr_stream_destroy_done" + : kind === "t" + ? valueData + ? "scr_stream_transform_done_value" + : "scr_stream_transform_done" + : valueData + ? "scr_stream_flush_done_value" + : "scr_stream_flush_done"; const twoSlot = (kind === "t" || kind === "l") && !valueData; host.declare( `declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`, @@ -623,7 +625,7 @@ export function zlibBytesThunkFor( export function streamCbThunkFor( host: LlvmEmitterContext, - kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", + kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v", cbT: IrType, ): string { if (cbT.kind !== "func") @@ -638,9 +640,11 @@ export function streamCbThunkFor( ? ["ptr %s", "double %size"] : kind === "w" || kind === "t" ? ["ptr %s", "ptr %chunk"] - : kind === "d" || kind === "e" - ? ["ptr %s", "ptr %err"] - : ["ptr %s"]; + : kind === "v" + ? ["ptr %s", "ptr %chunks"] + : kind === "d" || kind === "e" + ? ["ptr %s", "ptr %err"] + : ["ptr %s"]; const declared = cbT.params; const hasThis = declared[0] !== undefined && declared[0].kind === "object"; if (declared.length === 0) { @@ -649,7 +653,9 @@ export function streamCbThunkFor( ); } const off = hasThis ? 1 : 0; - const full = (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" ? 2 : 1) + off; + const full = + (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" || kind === "v" ? 2 : 1) + + off; if (declared.length > full) { throw new InternalCompilerError( `llvm emitter bug: stream '${kind}' callback declares ${declared.length} params (frontend must fence)`, @@ -697,6 +703,31 @@ export function streamCbThunkFor( } continue; } + if (kind === "v" && pos === 0) { + // writev's chunks: the batch array (borrowed from the runtime), as + // a dyn value or converted to the declared type + if (p.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); + d.push(` %vc${i} = call ptr @scr_dyn_retain_v(ptr %chunks)`); + passed.push(`ptr %vc${i}`); + } else { + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + d.push( + ` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`, + ` %vtbad${i} = call zeroext i1 @scr_exc_pending()`, + ` br i1 %vtbad${i}, label %vtfail${i}, label %vtok${i}`, + `vtfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `vtok${i}:`, + ); + passed.push(`${ty} %vt${i}`); + } + continue; + } const isChunkPos = (kind === "w" || kind === "t") && pos === 0; const isEncPos = (kind === "w" || kind === "t") && pos === 1; const isErrPos = (kind === "d" || kind === "e") && pos === 0; @@ -705,7 +736,7 @@ export function streamCbThunkFor( ? false : kind === "w" || kind === "t" ? pos === 2 - : kind === "f" || kind === "l" + : kind === "f" || kind === "l" || kind === "c" ? pos === 0 : pos === 1; if (p.kind === "dyn") { @@ -751,7 +782,7 @@ export function streamCbThunkFor( d.push(` br label %ego${i}`, `ego${i}:`, ` %edv${i} = load ptr, ptr %edslot${i}`); passed.push(`ptr %edv${i}`); } else if (isDonePos) { - const glue = `scr_stream_done_dyn_${kind}`; + const glue = `scr_stream_done_dyn_${kind === "v" ? "w" : kind}`; host.declare(`declare ptr @${glue}(ptr, ptr, ${host.sizeType})`); host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`); const clo = doneClosure(`@${glue}`, `dd${i}`); @@ -872,7 +903,8 @@ export function streamCbThunkFor( ); passed.push(`ptr %euv${i}`); } else if (isDonePos) { - const doneKind = kind as "w" | "f" | "d" | "t" | "l"; // "e" has no done position + // "e" has no done position; writev completes as a write + const doneKind = (kind === "v" ? "w" : kind) as "w" | "f" | "d" | "t" | "l" | "c"; if (p.kind !== "func") throw new InternalCompilerError("llvm emitter bug: stream done callback not a func"); const doneFn = host.streamDoneFnFor(doneKind, p); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..46bfc279e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1126,6 +1126,9 @@ export const LIB_FN_SYMS: Record = { "readable.pushStrEnc": "scr_stream_push_str_enc", "readable.pushEncoding": "scr_stream_set_push_encoding", "stream.objectMode": "scr_stream_object_mode", + "stream.decodeStrings": "scr_stream_decode_strings", + "writable.writeEncoding": "scr_stream_write_encoding", + "writable.setDefaultEncoding": "scr_stream_set_default_encoding", "stream.chunkDone": "scr_stream_chunk_done", "readable.pushNull": "scr_stream_push_null", "readable.pushDyn": "scr_stream_push_dyn", @@ -1729,6 +1732,9 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setDestroy", "stream.setTransform", "stream.setFlush", + "stream.setConstruct", + "stream.setWritev", + "stream.setWritevDefault", "process.activeResources", "stream.finished", "stream.finishedDyn", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 6d6c6f99c..4611b3bb8 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -187,18 +187,24 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.setFinal" || e.fn === "stream.setDestroy" || e.fn === "stream.setTransform" || - e.fn === "stream.setFlush" + e.fn === "stream.setFlush" || + e.fn === "stream.setConstruct" || + e.fn === "stream.setWritev" || + e.fn === "stream.setWritevDefault" ) { // The underscore-method assignment surface: the runtime slot swaps // its closure (+1 moves in) and invoke thunk — the next // _read/_write/... dispatch uses it, Node's timing. - const kindOf: Record = { + const kindOf: Record = { "stream.setRead": "r", "stream.setWrite": "w", "stream.setFinal": "f", "stream.setDestroy": "d", "stream.setTransform": "t", "stream.setFlush": "l", + "stream.setConstruct": "c", + "stream.setWritev": "v", + "stream.setWritevDefault": "v", }; const symOf: Record = { "stream.setRead": "scr_stream_set_read", @@ -207,6 +213,9 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setDestroy": "scr_stream_set_destroy", "stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush", + "stream.setConstruct": "scr_stream_set_construct", + "stream.setWritev": "scr_stream_set_writev", + "stream.setWritevDefault": "scr_stream_set_writev_default", }; const recv = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 703e8fe48..3a41d9176 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 26 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v26" as const; +export const RUNTIME_ABI_VERSION = 27 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index c884f1132..af61e5229 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 3a0b7870a..d09509911 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index fd4420b69..995cb177a 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1414,7 +1414,8 @@ const BUF_ENCODINGS: Record = { * does not know — the ladder callers (stream options) turn unknown * literals into Node's runtime ERR_UNKNOWN_ENCODING throw. */ export function knownBufEncoding(name: string): string | undefined { - return own(BUF_ENCODINGS, name); + // Buffer.isEncoding: any letter case + return own(BUF_ENCODINGS, name) ?? own(BUF_ENCODINGS, name.toLowerCase()); } /** The literal encoding argument of a Buffer surface, normalized — or a diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..682b9bde6 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -133,6 +133,7 @@ import { STREAM_PROP_MEMBERS, STREAM_COMPLETION_CALLBACK_INDEX, UNDERSCORE_METHODS, + streamExtraCallbacks, lowerStreamNew, lowerStreamSuperCall, streamCtorShape, @@ -2746,11 +2747,11 @@ export function collectClassShapeInner( `overriding the stream member '${name}' (the runtime owns the stream surface)`, ); } - if (name === "_writev" || name === "_construct") { + if (name === "_writev" && !streamExtraCallbacks(streamBase.def.name).includes("writev")) { lowerer.unsupported( "SC1090", memberName, - `declaring '${name}' on a stream subclass (${name === "_writev" ? "batched writes are" : "deferred construction is"} not lowered — writes deliver one chunk at a time)`, + `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`, ); } const accepted = streamCtorShape(streamBase.def.name).accepted; @@ -8987,8 +8988,9 @@ export function superCallStmt( // The SYNTHESIZED constructor of a ctor-less stream subclass: // super() with default options (underscore methods still bind; a // construction passing options requires a declared constructor — - // lowerNew fences that). Zero options ⇒ exactly one init stmt. - return lowerStreamSuperCall( + // lowerNew fences that). Zero options ⇒ the init stmt, then the + // class's _writev/_construct follow-ups. + const stmts = lowerStreamSuperCall( lowerer, info, base, @@ -8996,7 +8998,8 @@ export function superCallStmt( thisLocal, loc, info.decl ?? info.ctor!, - )[0]!; + ); + return stmts.length === 1 ? stmts[0]! : { kind: "block", body: stmts, loc }; } lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`); const call: IrStmt = { diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6540b3a99..20f156124 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -176,10 +176,25 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ ["flush", "_flush"], ]); +/** The option callbacks applied after construction rather than through + * the constructor's callback slots (option name → method name): construct + * on every stream class, and writev on the writable classes whose + * machinery hands it batches (Writable and Duplex). */ +export const STREAM_EXTRA_CALLBACKS: ReadonlyMap = new Map([ + ["construct", "_construct"], + ["writev", "_writev"], +]); + +export function streamExtraCallbacks(cls: string): readonly string[] { + return cls === "%Writable" || cls === "%Duplex" ? ["construct", "writev"] : ["construct"]; +} + /** The completion callback's parameter position in each underscore * method that takes one (cbTuples' "fn" positions). */ export const STREAM_COMPLETION_CALLBACK_INDEX: ReadonlyMap = new Map([ ["_write", 2], + ["_writev", 1], + ["_construct", 0], ["_final", 0], ["_destroy", 1], ["_transform", 2], @@ -317,9 +332,11 @@ function lowerStreamCallback( // (`string | null | undefined`) converts from the encoding argument's // value. if ( - ((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || + ((((i === 0 && + (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || i === 1) && - (which === "write" || which === "transform") && + (which === "write" || which === "transform")) || + (i === 0 && which === "writev")) && funcType.params[i]!.kind !== "func" && canConvertToDyn( funcType.params[i]!, @@ -429,6 +446,16 @@ interface StreamOptions { * right after construction. */ readableObject: boolean; writableObject: boolean; + /** decodeStrings: false (string writes stay strings), applied with + * stream.decodeStrings right after construction. */ + decodeStringsOff: boolean; + /** A writable side's non-utf8 `defaultEncoding` as written, applied + * with writable.writeEncoding. */ + writeEncoding: string | null; + /** The construct and writev callbacks (options or the class's + * _construct/_writev), applied with stream.setWritev and + * stream.setConstruct right after construction. */ + extras: { name: string; cb: IrExpr }[]; /** Present callbacks in canonical order with their flag bits. */ flags: number; cbs: IrExpr[]; @@ -470,14 +497,19 @@ function streamObjectModes( return out; } -/** Whether an options literal sets the writable side's object mode with a - * `true` literal (objectMode, or writableObjectMode on a duplex stream). - * Reports nothing: streamObjectModes fences the other value forms. */ +/** Whether an options literal makes the writable side hold values: its + * object mode set with a `true` literal (objectMode, or writableObjectMode + * on a duplex stream), or decodeStrings: false (strings stay strings). + * Reports nothing: the options parse fences the other value forms. */ function literalWritesValues(opts: ts.ObjectLiteralExpression, duplexish: boolean): boolean { for (const prop of opts.properties?.nodes ?? []) { if (!ts.isPropertyAssignment(prop)) continue; const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; + if (name === "decodeStrings") { + if (prop.initializer.kind !== ts.SyntaxKind.TrueKeyword) return true; + continue; + } if (name !== "objectMode" && !(duplexish && name === "writableObjectMode")) continue; if (prop.initializer.kind !== ts.SyntaxKind.FalseKeyword) return true; } @@ -494,6 +526,8 @@ function cbTuples(lowerer: Lowerer): Record { read: [F64], write: [BYTES, STRING, "fn"], final: ["fn"], + construct: ["fn"], + writev: [DYN, "fn"], destroy: [errorOrNull(lowerer), "fn"], transform: [BYTES, STRING, "fn"], flush: ["fn"], @@ -734,9 +768,14 @@ function parseStreamOptions( signal: null, readableObject: false, writableObject: false, + decodeStringsOff: false, + writeEncoding: null, + extras: [], flags: 0, cbs: [], }; + const extraNames = streamExtraCallbacks(`%${ctorName}`); + const writableClass = ctorName !== "Readable"; const followed = followOptionsArg(lowerer, arg); const optsLit = followed === null ? undefined : followed; if (optsLit !== undefined && !ts.isObjectLiteralExpression(optsLit)) { @@ -756,6 +795,12 @@ function parseStreamOptions( ); out.readableObject = objectModes.readable; out.writableObject = objectModes.writable; + // the chunk an option callback receives can be another value than a + // Buffer (literalWritesValues), read before any callback lowers + const writesValues = + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable"); if (out.readableObject) out.hwmR = numLit(16, loc); if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); @@ -772,7 +817,7 @@ function parseStreamOptions( lowerer.unsupported("SC1090", prop, `computed or spread members in ${ctorName} options`); } // Callback members: shorthand methods and function-valued properties. - if (accepted.includes(name)) { + if (accepted.includes(name) || extraNames.includes(name)) { let fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction | null = null; if (ts.isMethodDeclaration(prop)) fnNode = prop; else if ( @@ -823,7 +868,7 @@ function parseStreamOptions( }); present.set( name, - lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, out.writableObject), + lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues), ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); @@ -872,11 +917,17 @@ function parseStreamOptions( // read up front (streamObjectModes) break; case "decodeStrings": { + // WritableState's `options.decodeStrings === false`; a Readable + // ignores the option if (value.kind === ts.SyntaxKind.TrueKeyword) break; // Node's default + if (value.kind === ts.SyntaxKind.FalseKeyword) { + if (writableClass) out.decodeStringsOff = true; + break; + } lowerer.noLowering( - `${ctorName} with decodeStrings: false`, + `${ctorName} with a runtime 'decodeStrings' value`, prop, - "chunks always decode to Buffers here (Node's default)", + "write 'decodeStrings: true' or 'decodeStrings: false'", ); break; } @@ -911,15 +962,20 @@ function parseStreamOptions( out.badEncoding = t.value; break; } - if (t.isStringLiteralType() && ctorName === "Readable") { - out.pushEncoding = knownBufEncoding(t.value)!; + if (t.isStringLiteralType()) { + // the readable side decodes push(string) with it, and the + // writable side encodes write(string) with it (the name as + // written reaches _write when decodeStrings is false) + if (ctorName !== "Writable" && !out.readableObject) + out.pushEncoding = knownBufEncoding(t.value)!; + if (writableClass) out.writeEncoding = t.value; break; } } lowerer.noLowering( - `the ${ctorName} option 'defaultEncoding'`, + `the ${ctorName} option 'defaultEncoding' with a runtime value`, prop, - "utf8 (the default) is the supported write-side string encoding", + "write the encoding as a string literal", ); break; case "signal": { @@ -980,18 +1036,13 @@ function parseStreamOptions( ); break; } - case "construct": - lowerer.noLowering( - `the ${ctorName} option 'construct'`, - prop, - "deferred construction (_construct) is not lowered yet", - ); - break; case "writev": lowerer.noLowering( `the ${ctorName} option 'writev'`, prop, - "batched writes (_writev) are not lowered yet — writes deliver one chunk at a time", + ctorName === "Readable" + ? "a Readable has no writable side" + : "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex", ); break; default: @@ -1008,6 +1059,10 @@ function parseStreamOptions( out.cbs.push(cb); } } + for (const name of extraNames) { + const cb = present.get(name) ?? fallbackCb?.(name) ?? null; + if (cb) out.extras.push({ name, cb }); + } return out; } @@ -1029,6 +1084,9 @@ function streamCtorArgs( signal: IrExpr | null; readableObject: boolean; writableObject: boolean; + decodeStringsOff: boolean; + writeEncoding: string | null; + extras: { name: string; cb: IrExpr }[]; } { const shape = streamCtorShape(cls); const o = parseStreamOptions( @@ -1060,9 +1118,72 @@ function streamCtorArgs( signal: o.signal, readableObject: o.readableObject, writableObject: o.writableObject, + decodeStringsOff: o.decodeStringsOff, + writeEncoding: o.writeEncoding, + extras: o.extras, }; } +/** The follow-ups a stream's options apply over the fresh stream `recv` + * after the chained ones: decodeStrings, the writable defaultEncoding, + * then writev and construct (Node's constructors schedule construct + * last). */ +function streamOptionFollowUps( + parsed: { + decodeStringsOff: boolean; + writeEncoding: string | null; + extras: { name: string; cb: IrExpr }[]; + }, + recv: () => IrExpr, + thisType: IrType, + loc: SrcLoc, + writevFn: IrLibFn = "stream.setWritev", +): IrStmt[] { + const out: IrStmt[] = []; + if (parsed.decodeStringsOff) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.decodeStrings", + args: [recv(), boolLit(false, loc)], + type: thisType, + loc, + }, + loc, + }); + } + if (parsed.writeEncoding !== null) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "writable.writeEncoding", + args: [recv(), strLit(parsed.writeEncoding, loc)], + type: thisType, + loc, + }, + loc, + }); + } + for (const name of ["writev", "construct"]) { + const extra = parsed.extras.find((x) => x.name === name); + if (!extra) continue; + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: name === "writev" ? writevFn : "stream.setConstruct", + args: [recv(), extra.cb], + type: VOID, + loc, + }, + loc, + }); + } + return out; +} + /** `new Readable(opts?)` and friends — called from lowerNew once the * callee resolved to a builtin stream class. */ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: ClassInfo): IrExpr { @@ -1137,12 +1258,29 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (parsed.signal === null) return built; - // { signal }: addAbortSignal over the fresh stream (answers it +1). + if (parsed.signal !== null) { + // { signal }: addAbortSignal over the fresh stream (answers it +1). + built = { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [built, parsed.signal], + type: thisType, + loc, + }; + } + if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) + return built; + const fresh = lowerer.declareHiddenLocal("%stream", thisType); + const followUps = streamOptionFollowUps( + parsed, + () => varRef(fresh.id, thisType, loc), + thisType, + loc, + ); return { - kind: "libCall", - fn: "stream.addAbortSignal", - args: [built, parsed.signal], + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps], + result: varRef(fresh.id, thisType, loc), type: thisType, loc, }; @@ -1236,10 +1374,12 @@ function streamMethodWrapper( } else if (!typeEquals(got.type, want)) { // A write or transform encoding of another type (`string | null | // undefined`) converts from the encoding argument's value: an - // object-mode write passes undefined, null or the name. + // object-mode write passes undefined, null or the name. A chunk of + // another type (`string | Buffer` under decodeStrings: false) + // converts from the chunk's value. if ( - i === 1 && - (which === "write" || which === "transform") && + (i === 1 || (i === 0 && (chunkValues || which === "writev"))) && + (which === "write" || which === "transform" || which === "writev") && got.type.kind !== "func" && canConvertToDyn( got.type, @@ -1262,8 +1402,7 @@ function streamMethodWrapper( // or throws a TypeError there; a byte-mode side's chunk is a Buffer. if ( i === 0 && - chunkValues && - (which === "write" || which === "transform") && + (which === "writev" || (chunkValues && (which === "write" || which === "transform"))) && got.type.kind !== "func" && canConvertToDyn( got.type, @@ -1380,7 +1519,7 @@ export function lowerStreamSuperCall( (!ts.isObjectLiteralExpression(optsNode) || literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { - const methodName = UNDERSCORE_METHODS.get(name); + const methodName = UNDERSCORE_METHODS.get(name) ?? STREAM_EXTRA_CALLBACKS.get(name); return methodName ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame, chunkValues) : null; @@ -1401,6 +1540,13 @@ export function lowerStreamSuperCall( cbs.push(cb); } } + // The class's _writev and _construct ride the same way: an options + // writev or construct (applied by initDyn) wins. + const extras: { name: string; cb: IrExpr }[] = []; + for (const name of streamExtraCallbacks(cls)) { + const cb = fallback(name); + if (cb) extras.push({ name, cb }); + } return [ { kind: "exprStmt", @@ -1413,6 +1559,13 @@ export function lowerStreamSuperCall( }, loc, }, + ...streamOptionFollowUps( + { decodeStringsOff: false, writeEncoding: null, extras }, + thisRef, + thisType, + loc, + "stream.setWritevDefault", + ), ]; } } @@ -1499,6 +1652,7 @@ export function lowerStreamSuperCall( loc, }); } + out.push(...streamOptionFollowUps(parsed, thisRef, thisType, loc)); return out; } @@ -1542,18 +1696,18 @@ export function lowerStreamUnderscoreAssign( const loc = locOf(expr); const cls = base.def.name; const clsName = cls.slice(1); - if (methodName === "_writev" || methodName === "_construct") { + if (methodName === "_construct") { + // Node's constructor reads _construct once; a later assignment has no + // effect there lowerer.noLowering( `assigning '${methodName}' on a ${clsName}`, expr.left, - methodName === "_writev" - ? "batched writes (_writev) are not lowered yet — writes deliver one chunk at a time" - : "deferred construction (_construct) is not lowered yet", + "declare _construct on the class or pass the construct option", ); } - const which = optionByMethod.get(methodName)!; + const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); - if (!accepted.includes(which)) { + if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) { lowerer.noLowering( `assigning '${methodName}' on a ${clsName}`, expr.left, @@ -1589,6 +1743,7 @@ export function lowerStreamUnderscoreAssign( destroy: "stream.setDestroy", transform: "stream.setTransform", flush: "stream.setFlush", + writev: "stream.setWritev", }; return { kind: "exprStmt", @@ -4616,18 +4771,20 @@ export function lowerStreamMethodCall( } if (member === "setDefaultEncoding") { - // The write-side default: utf8 IS the default — admit it; other - // encodings would change what write(string) means. + // The write-side default: lowercased and validated (ERR_UNKNOWN_ENCODING, + // thrown) at run time; answers the stream. requireSide(writableSide, "setDefaultEncoding"); - const t = args[0] ? lowerer.typeOf(args[0]) : null; - if (t?.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8")) { - return lowerer.lowerExpr(access.expression); - } - lowerer.noLowering( - "setDefaultEncoding", - call, - "utf8 (the default, as a compile-time literal) is the supported write-side string encoding", - ); + if (args.length > 1) + lowerer.noLowering(`setDefaultEncoding with ${args.length} arguments`, call); + const receiver = lowerer.lowerExpr(access.expression); + const enc = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "writable.setDefaultEncoding", + args: [receiver, enc], + type: receiver.type, + loc, + }; } return null; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..6ddee1597 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -379,6 +379,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "writable.writeEnc", "writable.end", "writable.uncork", + "writable.setDefaultEncoding", "stream.destroy", "stream.destroyErr", "stream.iteratorClose", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..e561c0dbf 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3930,6 +3930,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.setDestroy": { argTypes: [null, null], result: VOID }, "stream.setTransform": { argTypes: [null, null], result: VOID }, "stream.setFlush": { argTypes: [null, null], result: VOID }, + "stream.setConstruct": { argTypes: [null, null], result: VOID }, + "stream.setWritev": { argTypes: [null, null], result: VOID }, + "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, /** The dyn-options twins (a checked-dynamic options record — the JS * lane's `super(options)` forwarding and `new Readable(dynVar)`): the * option walk runs at RUNTIME with Node's reading rules. newDyn: @@ -4056,6 +4059,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** The objectMode option's sides (readable, writable), applied right * after construction; answers the receiver (+1, the chaining shape). */ "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, + "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, + "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, /** Whether a readable.nextChunkDyn step answered the end (the done * marker; an object-mode stream can yield undefined). */ "stream.chunkDone": { argTypes: [DYN], result: BOOL }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5b984f318..555f5d306 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -218,6 +218,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "child.stdioStream", "stream.chunkDone", "stream.objectMode", + "stream.decodeStrings", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..68bc61b21 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5494,6 +5494,10 @@ function validateFunction( e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || e.fn === "stream.objectMode" || + e.fn === "stream.decodeStrings" || + e.fn === "stream.setConstruct" || + e.fn === "stream.setWritev" || + e.fn === "stream.setWritevDefault" || e.fn === "stream.iteratorClose" || e.fn === "stream.prop" || e.fn === "stream.errored" || @@ -5785,6 +5789,9 @@ function validateFunction( e.fn === "readable.setEncoding" || e.fn === "readable.pushEncoding" || e.fn === "stream.objectMode" || + e.fn === "stream.decodeStrings" || + e.fn === "writable.writeEncoding" || + e.fn === "writable.setDefaultEncoding" || e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || e.fn === "stream.iteratorClose" diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index af8d9b724..35a747d60 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v26"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v26"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json new file mode 100644 index 000000000..f1dcb625f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7246-stream-construct.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7246-stream-construct.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json new file mode 100644 index 000000000..00c6c7f0c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7247-stream-construct.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7247-stream-construct.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json new file mode 100644 index 000000000..0faf76b0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7248-stream-writev.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7248-stream-writev.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json new file mode 100644 index 000000000..5374e215e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7249-stream-writev.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7249-stream-writev.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json new file mode 100644 index 000000000..67d2b759b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7250-stream-decode-strings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7250-stream-decode-strings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json new file mode 100644 index 000000000..d4c01fab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7251-stream-write-encodings.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7251-stream-write-encodings.mjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 012b86013..631750cdf 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 5b8f3563f..0e81033ad 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 24cd8093b..a3ac43cc0 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v26(void) {} +void scr_runtime_abi_v27(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 791c600c7..fea020d16 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v26(void); +void scr_runtime_abi_v27(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index cdd815642..384e8d337 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -108,10 +108,6 @@ static ScrDyn *bcs_undefined(void) { return scr_dyn_retain(scr_dyn_undefined()); } -static ScrDyn *bcs_str(const char *text) { - return bcs_key(text); -} - static void bcs_unsupported(const char *message) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); } @@ -231,7 +227,7 @@ static ScrStream *bcs_need(ScrDyn *self, const char *reading) { /* ── completion callbacks ───────────────────────────────────────────── */ -enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH }; +enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH, BCS_DONE_CONSTRUCT }; static void bcs_run_destroy_cbs(ScrStream *s, ScrError *err) { BcsRec *rec = bcs_rec(s); @@ -266,6 +262,9 @@ static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t ar case BCS_DONE_FINAL: scr_stream_final_done(s, err); break; + case BCS_DONE_CONSTRUCT: + scr_stream_construct_done(s, err); + break; case BCS_DONE_DESTROY: { ScrError *kept = err ? scr_error_retain(err) : NULL; scr_stream_destroy_done(s, err); @@ -401,6 +400,30 @@ static void bcs_plain_call(ScrClosure *cb, ScrStream *s, const char *method, int static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed); +/* constructNT: `try { stream._construct(cb) } catch (err) { onConstruct(err) }`. */ +static void bcs_construct_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *callback = bcs_done(s, BCS_DONE_CONSTRUCT); + ScrDyn *result = bcs_invoke(object, "_construct", &callback, 1); + scr_dyn_release(callback); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) scr_stream_construct_done(s, bcs_take_error()); + scr_dyn_release(object); +} + +/* clearBuffer's batch: this._writev(chunks, cb); a throw reaches the + * write() caller, as in Node. */ +static void bcs_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *args[2]; + args[0] = scr_dyn_retain(chunks); + args[1] = bcs_done(s, BCS_DONE_WRITE); + ScrDyn *result = bcs_invoke(object, "_writev", args, 2); + for (int i = 0; i < 2; i++) scr_dyn_release(args[i]); + if (result) scr_dyn_release(result); + scr_dyn_release(object); +} + /* prefinish reads `stream._final` when the writable side finishes, so a * method assigned after the constructor runs; without one the finish goes * on as Node's does when `typeof stream._final !== 'function'`. */ @@ -875,14 +898,14 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } bool failed = false; bool writes = kind != BCS_READABLE; - if (writes && bcs_is_function(self, "_writev", &failed)) { - bcs_unsupported("_writev on a constructor-function stream is not supported yet"); - return NULL; - } - if (!failed && bcs_is_function(self, "_construct", &failed)) { - bcs_unsupported("_construct on a constructor-function stream is not supported yet"); + bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; + /* Node's constructors read _writev and _construct as they run */ + bool has_writev = writes && bcs_is_function(self, "_writev", &failed); + if (has_writev && transformish) { + bcs_unsupported("_writev on a constructor-function Transform is not supported yet"); return NULL; } + bool has_construct = !failed && bcs_is_function(self, "_construct", &failed); if (failed) return NULL; ScrDyn *opts = bcs_arg(args, argc, 0); ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) @@ -936,6 +959,9 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } } scr_stream_set_destroy(s, bcs_hook(self, (void *)bcs_destroy_inv), bcs_destroy_inv); + if (has_writev) scr_stream_set_writev(s, bcs_hook(self, (void *)bcs_writev_inv), bcs_writev_inv); + if (has_construct) + scr_stream_set_construct(s, bcs_hook(self, (void *)bcs_construct_inv), bcs_construct_inv); bcs_hide(self, s); if (!failed) failed = !bcs_adopt_listeners(s, self); if (!failed && kind != BCS_WRITABLE) failed = !bcs_state_view(self, s, true); @@ -1020,17 +1046,6 @@ static ScrClosure *bcs_callback(ScrDyn *fn, ScrDyn *self, ScrStream *s) { return closure; } -/* Strings in another encoding than utf8 have no write path here. */ -static bool bcs_utf8_only(const ScrDyn *encoding) { - if (encoding->kind != SCR_DYN_STR) return true; - ScrStr *canonical = bcs_encoding(encoding); - if (!canonical) return false; - bool utf8 = bcs_is(canonical, "utf8"); - scr_str_release(canonical); - if (!utf8) bcs_unsupported("writing strings in an encoding other than utf8 to a constructor-function stream is not supported yet"); - return utf8; -} - static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { ScrDyn *chunk = bcs_arg(args, argc, 0); ScrDyn *encoding = bcs_arg(args, argc, 1); @@ -1039,14 +1054,9 @@ static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t cb = encoding; encoding = scr_stream_writes_values(s) ? scr_dyn_new_null() : scr_dyn_undefined(); } - bool below; - if (scr_stream_writes_values(s)) { - /* an object-mode side hands the encoding argument to _write */ - below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); - } else { - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; - below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); - } + /* an object-mode side hands the encoding argument to _write; a byte-mode + * side encodes a string with it (or the default encoding) */ + bool below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s)); if (scr_exc_pending()) return NULL; return scr_dyn_new_bool(below); } @@ -1064,24 +1074,17 @@ static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t a encoding = scr_dyn_new_null(); } ScrStream *chained = NULL; - if (!bcs_nullish(chunk) && scr_stream_writes_values(s)) { - /* end(chunk) writes the value with the encoding argument */ + if (!bcs_nullish(chunk)) { + /* end(chunk) writes the chunk with the encoding argument first */ + if (scr_stream_prop(s, "writableObjectMode") == 0 && chunk->kind != SCR_DYN_STR && + chunk->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); + return NULL; + } scr_stream_write_enc(s, chunk, encoding, NULL); if (scr_exc_pending()) return NULL; - chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) { - return NULL; - } else if (bcs_nullish(chunk)) { - chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_STR) { - chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s)); - } else if (chunk->kind == SCR_DYN_BYTES) { - ScrBytes *bytes = scr_bytes_raw_view(chunk->v.bytes); - chained = scr_stream_end(s, bytes, NULL, bcs_callback(cb, self, s)); - scr_bytes_release(bytes); - } else { - scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); } + chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); if (chained) scr_stream_release(chained); if (scr_exc_pending()) return NULL; return scr_dyn_retain(self); @@ -1164,8 +1167,26 @@ static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t switch (method) { case BCS_M_READ_DEFAULT: return bcs_not_implemented("_read()"); - case BCS_M_WRITE_DEFAULT: + case BCS_M_WRITE_DEFAULT: { + /* Writable.prototype._write: this._writev([{ chunk, encoding }], cb) */ + ScrDyn *self = scr_dyn_this_get(); + bool failed = false; + if (bcs_is_function(self, "_writev", &failed)) { + ScrDyn *chunks = scr_dyn_new_arr(); + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "chunk", 5, scr_dyn_retain(bcs_arg(args, argc, 0))); + scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_retain(bcs_arg(args, argc, 1))); + scr_dyn_arr_push(chunks, entry); + ScrDyn *pass[2] = {chunks, bcs_arg(args, argc, 2)}; + ScrDyn *result = bcs_invoke(self, "_writev", pass, 2); + scr_dyn_release(chunks); + scr_dyn_release(self); + return result; + } + scr_dyn_release(self); + if (failed) return NULL; return bcs_not_implemented("_write()"); + } case BCS_M_TRANSFORM_DEFAULT: return bcs_not_implemented("_transform()"); case BCS_M_DESTROY_DEFAULT: @@ -1317,9 +1338,12 @@ static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t scr_stream_uncork(s); result = bcs_undefined(); break; - case BCS_M_SET_DEFAULT_ENCODING: - if (bcs_utf8_only(first)) result = scr_dyn_retain(self); + case BCS_M_SET_DEFAULT_ENCODING: { + ScrStream *chained = scr_stream_set_default_encoding(s, first); + if (chained) scr_stream_release(chained); + if (!scr_exc_pending()) result = scr_dyn_retain(self); break; + } default: bcs_unsupported("Readable.prototype.wrap on a constructor-function stream is not supported yet"); break; diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index be5ccb910..691b6af8e 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -186,6 +186,19 @@ struct ScrStreamState { * and transform steps receive, are ScrDyn* values (in the ScrBytes* * slots), each counting 1 toward length. */ bool object_mode; + /* The queue holds ScrDyn* values: on an object-mode side, and with + * decodeStrings: false, where strings stay strings (their UTF-16 + * length counts) and Buffers are buffer values. Each entry's encoding + * argument rides in ScrWEntry.enc. */ + bool values, decode_strings_off; + /* The defaultEncoding option or setDefaultEncoding(), as given; NULL + * is utf8. What write(string) encodes with, and the encoding _write + * receives for a string when decodeStrings is false. */ + ScrStr *default_enc; + /* _writev: more than one buffered write reaches it as one batch, and a + * stream without _write sends each write through it. */ + ScrClosure *writev_cb; + ScrStreamWritevInv writev_inv; bool ending, finished, prefinished, final_called, finish_scheduled; bool final_pending; /* _final called, its callback not yet run */ ScrClosure *inflight_cb; /* owned or NULL */ @@ -252,6 +265,7 @@ struct ScrStreamState { * path and _destroy wait. A destroy() in that window marks the stream * destroyed and runs its _destroy once construction completes. */ bool constructing, destroy_deferred; + bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */ ScrError *destroy_err; /* owned: the deferred destroy's error */ ScrClosure *construct_cb; ScrStreamPlainInv construct_inv; @@ -340,18 +354,25 @@ static ScrDyn *scr_stream_entry_box(void *entry, ScrEntryKind kind) { return d; } -/* Writable-queue chunks: Buffers, or dyn values on an object-mode side. */ +/* Writable-queue chunks: Buffers, or dyn values (w.values). A value + * counts 1 on an object-mode side, else its length (a string's UTF-16 + * length, as Node's chunk.length). */ static size_t scr_stream_wchunk_len(const ScrStreamState *st, ScrBytes *chunk) { - return st->w.object_mode ? 1 : chunk->len; + if (st->w.object_mode) return 1; + if (!st->w.values) return chunk->len; + ScrDyn *v = (ScrDyn *)chunk; + if (v->kind == SCR_DYN_STR) return (size_t)scr_str_utf16_len(v->v.str); + if (v->kind == SCR_DYN_BYTES) return v->v.bytes->len; + return 1; } static ScrBytes *scr_stream_wchunk_retain(const ScrStreamState *st, ScrBytes *chunk) { - if (st->w.object_mode) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); + if (st->w.values) return (ScrBytes *)scr_dyn_retain((ScrDyn *)chunk); return scr_bytes_retain(chunk); } static void scr_stream_wchunk_release(const ScrStreamState *st, ScrBytes *chunk) { - if (st->w.object_mode) scr_dyn_release((ScrDyn *)chunk); + if (st->w.values) scr_dyn_release((ScrDyn *)chunk); else scr_bytes_release(chunk); } @@ -377,6 +398,7 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (st->destroy_err) scr_error_release(st->destroy_err); if (!gc) { if (st->construct_cb) scr_closure_release(st->construct_cb); + if (st->w.writev_cb) scr_closure_release(st->w.writev_cb); if (st->r.read_cb) scr_closure_release(st->r.read_cb); if (st->w.write_cb) scr_closure_release(st->w.write_cb); if (st->w.final_cb) scr_closure_release(st->w.final_cb); @@ -390,13 +412,14 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) { for (size_t i = 0; i < st->drain_srcs.n; i++) scr_stream_release(st->drain_srcs.src[i]); } for (size_t i = st->w.head; i < st->w.n; i++) { - bool traced = st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); + bool traced = st->w.values && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk); if (!gc || !traced) scr_stream_wchunk_release(st, st->w.q[i].chunk); if (!gc && st->w.q[i].cb) scr_closure_release(st->w.q[i].cb); ScrDyn *enc = st->w.q[i].enc; if (enc && (!gc || !scr_stream_dyn_entry_traced(enc))) scr_dyn_release(enc); } free(st->w.q); + if (st->w.default_enc) scr_str_release(st->w.default_enc); free(st->w.end_cbs); free(st->fin.cb); free(st->fin.inv); @@ -426,6 +449,7 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { if (st->construct_cb) visit(st->construct_cb, ctx); if (st->w.write_cb) visit(st->w.write_cb, ctx); if (st->w.final_cb) visit(st->w.final_cb, ctx); + if (st->w.writev_cb) visit(st->w.writev_cb, ctx); if (st->w.inflight_cb) visit(st->w.inflight_cb, ctx); if (st->transform_cb) visit(st->transform_cb, ctx); if (st->flush_cb) visit(st->flush_cb, ctx); @@ -434,7 +458,7 @@ void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { for (size_t i = 0; i < st->fin.n; i++) visit(st->fin.cb[i], ctx); for (size_t i = st->w.head; i < st->w.n; i++) { if (st->w.q[i].cb) visit(st->w.q[i].cb, ctx); - if (st->w.object_mode && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) + if (st->w.values && scr_stream_dyn_entry_traced((ScrDyn *)st->w.q[i].chunk)) visit(st->w.q[i].chunk, ctx); if (st->w.q[i].enc && scr_stream_dyn_entry_traced(st->w.q[i].enc)) visit(st->w.q[i].enc, ctx); } @@ -1032,23 +1056,10 @@ static void scr_stream_resume_kick(ScrStream *s) { static ScrError *scr_stream_dyn_err(ScrDyn *const *args, size_t argc) { if (argc < 1) return NULL; const ScrDyn *a = args[0]; - if (a->kind == SCR_DYN_UNDEF || a->kind == SCR_DYN_NULL) return NULL; - if (a->kind == SCR_DYN_STR) { - ScrError *e = scr_error_new(SCR_ERR_ERROR, a->v.str); - return e; - } - if (a->kind == SCR_DYN_OBJ) { - ScrDyn *m = scr_dyn_obj_get((ScrDyn *)a, "message", 7); - ScrStr *msg = m && m->kind == SCR_DYN_STR ? scr_str_retain(m->v.str) : scr_str_new("", 0); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - return e; - } - /* any other truthy value: Node would wrap it — approximate with "" */ - ScrStr *empty = scr_str_new("", 0); - ScrError *e = scr_error_new(SCR_ERR_ERROR, empty); - scr_str_release(empty); - return e; + if (!scr_dyn_truthy(a)) return NULL; + if (a->kind == SCR_DYN_STR) return scr_error_new(SCR_ERR_ERROR, a->v.str); + /* the error value itself (the same object reaches 'error') */ + return scr_error_from_dyn(a); } static ScrStream *scr_stream_dyn_recv(ScrClosure *clo) { @@ -1071,6 +1082,13 @@ ScrDyn *scr_stream_done_dyn_f(ScrClosure *clo, ScrDyn *const *args, size_t argc) return scr_stream_done_dyn_ret(); } +ScrDyn *scr_stream_done_dyn_c(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_stream_dyn_recv(clo); + scr_stream_construct_done(s, scr_stream_dyn_err(args, argc)); + scr_stream_release(s); + return scr_stream_done_dyn_ret(); +} + ScrDyn *scr_stream_done_dyn_d(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); scr_stream_destroy_done(s, scr_stream_dyn_err(args, argc)); @@ -1865,6 +1883,8 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc static void scr_stream_transform_callback(ScrStream *s); static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/); +static ScrDyn *scr_stream_wentry_value(const ScrStreamState *st, ScrBytes *chunk); +static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc); /* One write reaching _write/_transform; the callback reads the write's * encoding argument through w.cur_enc while it runs. */ @@ -1891,7 +1911,7 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ - if (st->w.object_mode) scr_stream_push_dyn(s, (ScrDyn *)chunk); + if (st->w.values) scr_stream_push_dyn(s, (ScrDyn *)chunk); else scr_stream_add_chunk(s, chunk, false); scr_stream_wchunk_release(st, chunk); if (!scr_exc_pending()) scr_stream_transform_callback(s); @@ -1912,6 +1932,19 @@ static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, S st->w.wsync = false; return; } + if (st->w.write_cb == NULL && st->w.writev_cb) { + /* Writable.prototype._write: this._writev([{ chunk, encoding }], cb) */ + ScrDyn *chunks = scr_dyn_new_arr(); + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "chunk", 5, scr_stream_wentry_value(st, chunk)); + scr_dyn_obj_set(entry, "encoding", 8, scr_stream_wentry_encoding(st, st->w.cur_enc)); + scr_dyn_arr_push(chunks, entry); + scr_stream_wchunk_release(st, chunk); + st->w.writev_inv(st->w.writev_cb, s, chunks); + scr_dyn_release(chunks); + st->w.wsync = false; + return; + } if (st->w.write_cb == NULL) { scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _write() method is not implemented"); @@ -2031,11 +2064,107 @@ static SCR_COLD void scr_stream_writev_buffer(ScrStream *s) { scr_stream_do_write(s, all, cbs, NULL); } +/* A write's chunk and encoding as _writev receives them (+1 each). */ +static ScrDyn *scr_stream_wentry_value(const ScrStreamState *st, ScrBytes *chunk) { + if (st->w.values) return scr_dyn_retain((ScrDyn *)chunk); + return chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); +} + +static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc) { + if (!st->w.values) { + ScrStr *buffer = scr_str_new("buffer", 6); + ScrDyn *d = scr_dyn_new_str(buffer); + scr_str_release(buffer); + return d; + } + return scr_dyn_retain(enc ? enc : scr_dyn_undefined()); +} + +/* A batch entry's callback: the write's callback, or Node's nop. */ +static ScrDyn *scr_stream_writev_entry_cb_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + if (clo->ncaps > 0) { + ScrClosure *cb = scr_box_get_ref(clo->caps[0]); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_closure_release(cb); + } + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *scr_stream_writev_entry_cb(ScrClosure *cb /*borrowed or NULL*/) { + ScrClosure *c = scr_closure_new((void *)&scr_stream_writev_entry_cb_call, cb ? 1 : 0); + if (cb) { + c->caps[0] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, &scr_closure_trace_v); + scr_box_set_ref(c->caps[0], scr_closure_retain(cb)); + } + return scr_dyn_new_func(c, &scr_stream_writev_entry_cb_call, 1, "(error)", cb ? "" : "nop"); +} + +/* Calls _writev with chunks as the in-flight write of len, whose + * completion runs cb (moves; NULL when every write had none). */ +static void scr_stream_writev_call(ScrStream *s, ScrDyn *chunks, size_t len, ScrClosure *cb) { + ScrStreamState *st = s->st; + st->w.writing = true; + st->w.wsync = true; + st->w.inflight_len = len; + st->w.inflight_cb = cb; + st->w.writev_inv(st->w.writev_cb, s, chunks); + st->w.wsync = false; +} + +/* Node's clearBuffer with _writev and more than one buffered write: the + * writes go out as one [{ chunk, encoding, callback }] batch (its + * allBuffers says whether every encoding is 'buffer'), and the batch's + * completion runs the writes' callbacks in order. */ +static void scr_stream_writev_batch(ScrStream *s) { + ScrStreamState *st = s->st; + size_t total = 0, ncbs = 0; + for (size_t i = st->w.head; i < st->w.n; i++) + if (st->w.q[i].cb) ncbs++; + ScrClosure *cbs = ncbs > 0 ? scr_closure_new((void *)&scr_stream_writev_cbs, ncbs) : NULL; + ScrDyn *chunks = scr_dyn_new_arr(); + bool all_buffers = true; + size_t k = 0; + for (size_t i = st->w.head; i < st->w.n; i++) { + ScrWEntry e = st->w.q[i]; + total += scr_stream_wchunk_len(st, e.chunk); + ScrDyn *entry = scr_dyn_new_obj(); + ScrDyn *encoding = scr_stream_wentry_encoding(st, e.enc); + if (!(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 6 && + memcmp(encoding->v.str->data, "buffer", 6) == 0)) + all_buffers = false; + scr_dyn_obj_set(entry, "chunk", 5, scr_stream_wentry_value(st, e.chunk)); + scr_dyn_obj_set(entry, "encoding", 8, encoding); + scr_dyn_obj_set(entry, "callback", 8, scr_stream_writev_entry_cb(e.cb)); + scr_dyn_arr_push(chunks, entry); + scr_stream_wchunk_release(st, e.chunk); + if (e.enc) scr_dyn_release(e.enc); + if (e.cb) { + cbs->caps[k] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, + &scr_closure_trace_v); + scr_box_set_ref(cbs->caps[k], e.cb); /* moves */ + k++; + } + } + scr_dyn_arr_named_put(chunks, "allBuffers", 10, scr_dyn_new_bool(all_buffers)); + st->w.head = 0; + st->w.n = 0; + scr_stream_writev_call(s, chunks, total, cbs); + scr_dyn_release(chunks); +} + /* The buffered writes, one at a time (Node's clearBuffer without _writev). */ static void scr_stream_clear_buffer_each(ScrStream *s) { ScrStreamState *st = s->st; if (st->w.buffer_processing) return; st->w.buffer_processing = true; + if (st->w.writev_cb && !st->w.writing && st->w.n - st->w.head > 1 && st->w.corked == 0 && + !st->destroyed && !st->constructing && !scr_exc_pending()) { + scr_stream_writev_batch(s); + st->w.buffer_processing = false; + return; + } while (!st->w.writing && st->w.head < st->w.n && st->w.corked == 0 && !st->destroyed && !scr_exc_pending()) { ScrWEntry e = st->w.q[st->w.head++]; @@ -2528,6 +2657,27 @@ static void scr_stream_dynopt_transform_inv(ScrClosure *cb, ScrStream *s, ScrByt if (r != NULL) scr_dyn_release(r); } +static void scr_stream_dynopt_construct_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *fn = scr_stream_dynopt_target(cb); + ScrDyn *a = scr_stream_dynopt_done(s, &scr_stream_done_dyn_c, 1, "(error)"); + ScrDyn *r = scr_dyn_call(fn, &a, 1, "construct"); + scr_dyn_release(a); + scr_dyn_release(fn); + if (r != NULL) scr_dyn_release(r); +} + +static void scr_stream_dynopt_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks) { + ScrDyn *fn = scr_stream_dynopt_target(cb); + ScrDyn *args[2]; + args[0] = scr_dyn_retain(chunks); + args[1] = scr_stream_dynopt_done(s, &scr_stream_done_dyn_w, 1, "(error)"); + ScrDyn *r = scr_dyn_call(fn, args, 2, "writev"); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + scr_dyn_release(fn); + if (r != NULL) scr_dyn_release(r); +} + static void scr_stream_dynopt_final_inv(ScrClosure *cb, ScrStream *s) { ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *a = scr_stream_dynopt_done(s, &scr_stream_done_dyn_f, 1, "(error)"); @@ -2570,9 +2720,13 @@ typedef struct { bool auto_destroy, emit_close, allow_half_open; bool readable_side, writable_side; bool readable_object, writable_object; /* objectMode and the sided keys */ + bool decode_strings_off; /* decodeStrings: false */ ScrStr *enc; /* owned canonical name, or NULL */ + /* defaultEncoding other than utf8: as given (borrowed) and canonical + * (owned), or NULL */ + ScrStr *default_enc, *default_canonical; /* BORROWED callable fields (owned by the options record). */ - ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush; + ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush, *construct, *writev; } ScrStreamDynOpts; /* Node's `=== false` comparisons (autoDestroy/emitClose/allowHalfOpen and @@ -2647,7 +2801,10 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->writable_side = true; out->enc = NULL; out->readable_object = out->writable_object = false; + out->decode_strings_off = false; + out->default_enc = out->default_canonical = NULL; out->read = out->write = out->final_fn = out->destroy = out->transform = out->flush = NULL; + out->construct = out->writev = NULL; if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true; /* Node's `if (options)` */ const bool duplexish = base == 'd' || base == 't' || base == 'p'; const bool transformish = base == 't' || base == 'p'; @@ -2658,10 +2815,9 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->readable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "readableObjectMode", 18)); out->writable_object = object || (duplexish && scr_stream_dynopt_truthy(opts, "writableObjectMode", 18)); } - if (scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13))) { - static const char m[] = "decodeStrings: false is not supported yet (chunks always decode to Buffers)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } + /* WritableState: decodeStrings === false keeps string writes */ + out->decode_strings_off = + base != 'r' && scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "decodeStrings", 13)); { ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "signal", 6); if (v != NULL && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { @@ -2669,21 +2825,22 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy return scr_stream_dynopt_unsupported(m, sizeof m - 1); } } - if (scr_stream_dynopt_fn(opts, "construct", 9) != NULL) { - static const char m[] = "the stream option 'construct' is not supported yet (deferred construction has no lowering)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } - if (scr_stream_dynopt_fn(opts, "writev", 6) != NULL) { - static const char m[] = "the stream option 'writev' is not supported yet (writes deliver one chunk at a time)"; + out->construct = scr_stream_dynopt_fn(opts, "construct", 9); + if (base != 'r') out->writev = scr_stream_dynopt_fn(opts, "writev", 6); + if (transformish && out->writev != NULL) { + static const char m[] = "the stream option 'writev' on a Transform is not supported yet (batches would bypass the transform)"; return scr_stream_dynopt_unsupported(m, sizeof m - 1); } { + /* a known encoding or ERR_UNKNOWN_ENCODING (the states' constructors) */ ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "defaultEncoding", 15); if (v != NULL && v->kind == SCR_DYN_STR && !(v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) && !(v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0)) { - static const char m[] = "the stream option 'defaultEncoding' is not supported yet (utf8, the default, is the write-side string encoding)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); + ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str); + if (canonical == NULL) return false; + out->default_enc = v->v.str; + out->default_canonical = canonical; } } if (transformish) { @@ -2752,8 +2909,33 @@ static ScrClosure *scr_stream_dynopt_pick(ScrDyn *opt, void *dyn_inv, ScrClosure return fb; } -static void scr_stream_dynopt_apply_object(ScrStream *s, const ScrStreamDynOpts *o) { - if (s != NULL && s->st != NULL) scr_stream_set_object_mode(s, o->readable_object, o->writable_object); +static void scr_stream_dynopt_construct_inv(ScrClosure *cb, ScrStream *s); +static void scr_stream_dynopt_writev_inv(ScrClosure *cb, ScrStream *s, ScrDyn *chunks); + +/* The options a dyn constructor applies over the fresh stream: the object + * modes, decodeStrings, defaultEncoding (the push decoding of a readable + * side and the writes of a writable side), writev, and construct, which + * Node's constructor schedules last. Releases the owned canonical name. */ +static void scr_stream_dynopt_apply_object(ScrStream *s, ScrStreamDynOpts *o) { + if (s != NULL && s->st != NULL) { + scr_stream_set_object_mode(s, o->readable_object, o->writable_object); + if (o->decode_strings_off) scr_stream_set_decode_strings(s, false); + if (o->default_enc) { + scr_stream_set_write_encoding(s, o->default_enc); + if (s->st->has_r && !s->st->r.object_entries) + scr_stream_release(scr_stream_set_push_encoding(s, o->default_canonical)); + } + if (o->writev && s->st->has_w) + scr_stream_set_writev(s, scr_stream_dynopt_clo(o->writev, (void *)&scr_stream_dynopt_writev_inv), + &scr_stream_dynopt_writev_inv); + if (o->construct) + scr_stream_set_construct(s, scr_stream_dynopt_clo(o->construct, (void *)&scr_stream_dynopt_construct_inv), + &scr_stream_dynopt_construct_inv); + } + if (o->default_canonical) { + scr_str_release(o->default_canonical); + o->default_canonical = NULL; + } } static void scr_stream_dynopt_apply_enc(ScrStream *s, ScrStr *enc) { @@ -5542,7 +5724,69 @@ void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable) { st->r.object_entries = true; st->r.object_mode_opt = true; } - if (writable && st->has_w) st->w.object_mode = true; + if (writable && st->has_w) st->w.object_mode = st->w.values = true; +} + +/* decodeStrings: false — string writes stay strings (scr_stream_write_string). */ +void scr_stream_set_decode_strings(ScrStream *s, bool decode) { + ScrStreamState *st = s->st; + if (decode || !st->has_w) return; + st->w.decode_strings_off = true; + st->w.values = true; +} + +ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode) { + scr_stream_set_decode_strings(s, decode); + return scr_stream_retain(s); +} + +static void scr_stream_store_default_enc(ScrStreamState *st, ScrStr *enc /*moves*/) { + if (st->w.default_enc) scr_str_release(st->w.default_enc); + st->w.default_enc = NULL; + if ((enc->len == 4 && memcmp(enc->data, "utf8", 4) == 0) || + (enc->len == 5 && memcmp(enc->data, "utf-8", 5) == 0)) { + scr_str_release(enc); /* Node's kDefaultUTF8Encoding */ + return; + } + st->w.default_enc = enc; +} + +/* The defaultEncoding option on the writable side, a known encoding as + * given (borrowed; validated by the caller). */ +void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc) { + if (s->st->has_w) scr_stream_store_default_enc(s->st, scr_str_retain(enc)); +} + +ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc) { + scr_stream_set_write_encoding(s, enc); + return scr_stream_retain(s); +} + +/* setDefaultEncoding(encoding): the name lowercased, a known encoding or + * ERR_UNKNOWN_ENCODING (thrown); answers the stream (+1). */ +ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) { + ScrStr *raw = enc->kind == SCR_DYN_STR ? scr_str_retain(enc->v.str) : scr_dyn_string_coerce_js(enc); + ScrStr *lower = raw; + if (enc->kind == SCR_DYN_STR) { + char *buf = malloc(raw->len ? raw->len : 1); + if (!buf) scr_stream_oom(); + for (size_t i = 0; i < raw->len; i++) { + char c = raw->data[i]; + buf[i] = c >= 'A' && c <= 'Z' ? (char)(c - 'A' + 'a') : c; + } + lower = scr_str_new(buf, raw->len); + free(buf); + scr_str_release(raw); + } + ScrStr *canonical = scr_bytes_normalize_encoding(lower); + if (canonical == NULL) { + scr_str_release(lower); + return scr_stream_retain(s); + } + scr_str_release(canonical); + if (s->st->has_w) scr_stream_store_default_enc(s->st, lower); + else scr_str_release(lower); + return scr_stream_retain(s); } /* stream.objectMode: scr_stream_set_object_mode, answering the receiver @@ -5556,7 +5800,7 @@ ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { * object-mode writable side): the compiler's chunk thunks read the chunk * by this. */ bool scr_stream_writes_values(ScrStream *s) { - return s->st->has_w && s->st->w.object_mode; + return s->st->has_w && s->st->w.values; } /* emit('data', value) with a checked-dynamic chunk: the 'data' slots by @@ -6050,6 +6294,60 @@ static bool scr_stream_write_value(ScrStream *s, ScrDyn *v, ScrClosure *cb) { return scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, scr_stream_cb_enc(cb)); } +/* A Buffer write on a byte-mode side: with decodeStrings: false the + * queue holds values, so the Buffer is boxed with its 'buffer' encoding. + * Borrows chunk. */ +static bool scr_stream_write_buffer(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { + ScrStreamState *st = s->st; + if (!st->has_w || !st->w.values) return scr_stream_write_chunk(s, chunk, cb); + ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); + ScrStr *buffer = scr_str_new("buffer", 6); + ScrDyn *enc = scr_dyn_new_str(buffer); + scr_str_release(buffer); + bool ret = scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, enc); + scr_dyn_release(enc); + scr_dyn_release(v); + return ret; +} + +/* A string write on a byte-mode side, with the encoding argument as + * given (borrowed; NULL takes the default encoding): decodeStrings + * encodes it (Buffer.from(chunk, encoding)), and with decodeStrings: + * false it stays a string that _write receives with that encoding. */ +static bool scr_stream_write_string(ScrStream *s, ScrStr *str, ScrStr *name, ScrClosure *cb) { + ScrStreamState *st = s->st; + ScrStr *given = name ? scr_str_retain(name) : st->has_w && st->w.default_enc ? scr_str_retain(st->w.default_enc) : NULL; + if (st->has_w && st->w.decode_strings_off) { + ScrStr *e = given ? given : scr_str_new("utf8", 4); + ScrDyn *v = scr_dyn_new_str(str); + ScrDyn *enc = scr_dyn_new_str(e); + scr_str_release(e); + bool ret = scr_stream_write_chunk_enc(s, (ScrBytes *)v, cb, enc); + scr_dyn_release(enc); + scr_dyn_release(v); + return ret; + } + ScrBytes *b; + if (given) { + ScrStr *canonical = scr_bytes_normalize_encoding(given); + scr_str_release(given); + if (canonical == NULL) { + if (cb) scr_closure_release(cb); + return false; + } + b = scr_bytes_from_str(str, canonical); + scr_str_release(canonical); + } else { + b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); + memcpy(b->data, str->data, str->len); + if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); + } + scr_stream_buffer(b); + bool ret = scr_stream_write_buffer(s, b, cb); + scr_bytes_release(b); + return ret; +} + bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { if (s->st->has_w && s->st->w.object_mode) { ScrDyn *v = chunk->is_buffer ? scr_dyn_new_buffer(chunk) : scr_dyn_new_bytes(chunk); @@ -6057,7 +6355,7 @@ bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb) { scr_dyn_release(v); return ret; } - return scr_stream_write_chunk(s, chunk, cb); + return scr_stream_write_buffer(s, chunk, cb); } /* write(null) reached at runtime (a nullable-union chunk's null arm): @@ -6077,13 +6375,7 @@ bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { scr_dyn_release(v); return ret; } - ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)str->len); - memcpy(b->data, str->data, str->len); - if (!str->ascii) scr_wtf8_to_utf8((char *)b->data, str->len); - scr_stream_buffer(b); - bool ret = scr_stream_write_chunk(s, b, cb); - scr_bytes_release(b); - return ret; + return scr_stream_write_string(s, str, NULL, cb); } /* write(chunk, encoding, cb) with the encoding argument as passed (NULL @@ -6103,20 +6395,15 @@ bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClo name = scr_dyn_string_coerce_js(enc); if (name && !(name->len == 6 && memcmp(name->data, "buffer", 6) == 0)) { ScrStr *canonical = scr_bytes_normalize_encoding(name); - scr_str_release(name); if (canonical == NULL) { + scr_str_release(name); if (cb) scr_closure_release(cb); return false; } - if (chunk->kind != SCR_DYN_STR) { - scr_str_release(canonical); - return scr_stream_write_dyn(s, chunk, cb); - } - ScrBytes *b = scr_bytes_from_str(chunk->v.str, canonical); scr_str_release(canonical); - scr_stream_buffer(b); - bool ret = scr_stream_write_chunk(s, b, cb); - scr_bytes_release(b); + bool ret = chunk->kind == SCR_DYN_STR ? scr_stream_write_string(s, chunk->v.str, name, cb) + : scr_stream_write_dyn(s, chunk, cb); + scr_str_release(name); return ret; } if (name && chunk->kind == SCR_DYN_STR) { @@ -6535,8 +6822,41 @@ static void scr_stream_emit_readable_now(ScrStream *s) { /* ── tick dispatch ────────────────────────────────────────────────────── */ /* onConstruct + the readable/writable onConstructed hooks. err moves. */ +/* aggregateTwoErrors(inner, outer) for streams without native ops: both + * present and distinct make an AggregateError([outer, inner]) with + * outer's message and code. Borrows both; +1 or NULL. */ +static ScrError *scr_stream_aggregate_errors(ScrError *inner, ScrError *outer) { + if (inner && outer && inner != outer) { + ScrDyn *message = scr_dyn_new_str(outer->message); + ScrError *agg = scr_aggregate_new(message, scr_dyn_undefined()); + scr_dyn_release(message); + if (agg == NULL) return scr_error_retain(outer); + ScrDyn *outer_value = scr_dyn_from_error(outer); + ScrDyn *list = scr_dyn_new_arr(); + scr_dyn_arr_push(list, scr_dyn_retain(outer_value)); + scr_dyn_arr_push(list, scr_dyn_from_error(inner)); + scr_aggregate_set_errors(agg, list); + scr_dyn_release(list); + ScrDyn *code = scr_dyn_obj_read(outer_value, "code", 4); + if (code && code->kind == SCR_DYN_STR) scr_error_set_code(agg, code->v.str->data); + scr_dyn_release(code); + scr_dyn_release(outer_value); + return agg; + } + ScrError *one = outer ? outer : inner; + return one ? scr_error_retain(one) : NULL; +} + static void scr_stream_constructed(ScrStream *s, ScrError *err) { ScrStreamState *st = s->st; + if (st->construct_finished) { + /* onConstruct called again: errorOrDestroy(err ?? ERR_MULTIPLE_CALLBACK) */ + if (!err) err = scr_stream_mkerr("ERR_MULTIPLE_CALLBACK", "Callback called multiple times"); + scr_stream_error_or_destroy(s, err); + scr_error_release(err); + return; + } + st->construct_finished = true; st->constructing = false; if (st->construct_cb) { scr_closure_release(st->construct_cb); @@ -6549,8 +6869,9 @@ static void scr_stream_constructed(ScrStream *s, ScrError *err) { /* aggregateTwoErrors(constructError, destroyError): the owner builds * the AggregateError when both are present and distinct */ ScrError *use; - if (err && derr && err != derr && st->native_ops && st->native_ops->aggregate) { - use = st->native_ops->aggregate(err, derr); + if (err && derr && err != derr) { + use = st->native_ops && st->native_ops->aggregate ? st->native_ops->aggregate(err, derr) + : scr_stream_aggregate_errors(err, derr); } else { use = derr ? derr : err; if (use) scr_error_retain(use); @@ -6589,6 +6910,39 @@ void scr_stream_construct_done(ScrStream *s, ScrError *err /*moves*/) { void scr_stream_set_writev_concat(ScrStream *s) { s->st->writev_concat = true; } +/* A user construct option or _construct method: Node schedules it from the + * constructor (construct(stream, cb)), and the hook's callback reports + * through scr_stream_construct_done. */ +void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) { + if (s->st->construct_cb || s->st->construct_finished) { + scr_closure_release(cb); + return; + } + scr_stream_construct(s, cb, inv); +} + +/* A class's _writev under options that may carry their own writev (the + * option, an instance property in Node, wins). */ +void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { + if (s->st->has_w && s->st->w.writev_cb == NULL) { + scr_stream_set_writev(s, cb, inv); + return; + } + scr_closure_release(cb); +} + +/* A user writev option or _writev method. */ +void scr_stream_set_writev(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { + ScrStreamState *st = s->st; + if (!st->has_w) { + scr_closure_release(cb); + return; + } + if (st->w.writev_cb) scr_closure_release(st->w.writev_cb); + st->w.writev_cb = cb; + st->w.writev_inv = inv; +} + void scr_stream_fail(ScrStream *s, ScrError *err) { scr_stream_error_or_destroy(s, err); } static void scr_stream_plain_inv(ScrClosure *cb, va_list ap) { @@ -6780,6 +7134,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { if (st->r.read_cb) { scr_closure_release(st->r.read_cb); st->r.read_cb = NULL; } if (st->w.write_cb) { scr_closure_release(st->w.write_cb); st->w.write_cb = NULL; } if (st->w.final_cb) { scr_closure_release(st->w.final_cb); st->w.final_cb = NULL; } + if (st->w.writev_cb) { scr_closure_release(st->w.writev_cb); st->w.writev_cb = NULL; } if (st->transform_cb) { scr_closure_release(st->transform_cb); st->transform_cb = NULL; } if (st->flush_cb) { scr_closure_release(st->flush_cb); st->flush_cb = NULL; } if (st->destroy_cb) { scr_closure_release(st->destroy_cb); st->destroy_cb = NULL; } @@ -6834,7 +7189,11 @@ static void scr_stream_run_tick(ScrStreamTick *t) { scr_stream_error_buffer(s); break; case SCR_ST_CONSTRUCT: { - if (st->construct_cb) st->construct_inv(st->construct_cb, s); + if (st->construct_cb) { + st->construct_inv(st->construct_cb, s); + /* a throw inside _construct reaches onConstruct on a tick */ + if (scr_exc_pending()) scr_stream_construct_done(s, scr_stream_take_error()); + } break; } case SCR_ST_CONSTRUCTED: { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 8b8a72406..c78a03e1a 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -65,6 +65,8 @@ typedef void (*ScrStreamReadInv)(ScrClosure *cb, ScrStream *s, double size); typedef void (*ScrStreamChunkInv)(ScrClosure *cb, ScrStream *s, ScrBytes *chunk); typedef void (*ScrStreamPlainInv)(ScrClosure *cb, ScrStream *s); typedef void (*ScrStreamErrInv)(ScrClosure *cb, ScrStream *s, ScrError *err); +/* writev(chunks, callback): chunks is the batch's dyn array (borrowed). */ +typedef void (*ScrStreamWritevInv)(ScrClosure *cb, ScrStream *s, struct ScrDyn *chunks); /* Constructors (+1 result; option callbacks MOVE; NULL = absent). hwm < 0 * means the byte default (65536 — Node 24's getDefaultHighWaterMark). */ @@ -512,6 +514,21 @@ void scr_stream_flush_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); void scr_stream_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_construct_done(ScrStream *s, ScrError *err); void scr_stream_set_writev_concat(ScrStream *s); +/* The user construct option / _construct method (scheduled now; cb + * moves), and the writev option / _writev method (cb moves). */ +void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); +void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +/* decodeStrings (false keeps string writes as strings), the writable + * defaultEncoding option (a known encoding, borrowed), and + * setDefaultEncoding (validated; answers the stream +1). The chaining + * forms answer the stream (+1). */ +void scr_stream_set_decode_strings(ScrStream *s, bool decode); +ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode); +void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc); +ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc); +ScrStream *scr_stream_set_default_encoding(ScrStream *s, struct ScrDyn *enc); +ScrDyn *scr_stream_done_dyn_c(ScrClosure *clo, ScrDyn *const *args, size_t argc); /* The native state's extra operations: view adds its members to the * stream's untyped view and close is the view's close() (both NULL when * absent); aggregate builds aggregateTwoErrors' +1 AggregateError from the diff --git a/tests/corpus/7246-stream-construct.ts b/tests/corpus/7246-stream-construct.ts new file mode 100644 index 000000000..7cfb82e49 --- /dev/null +++ b/tests/corpus/7246-stream-construct.ts @@ -0,0 +1,156 @@ +// Deferred construction in typed code: the construct option and the +// _construct method on Readable, Writable, Duplex and Transform. Writes +// and reads wait for the callback, an error from it destroys the stream, +// a destroy while constructing reports both errors as an AggregateError, +// a throw inside construct is its error, and a second callback is +// ERR_MULTIPLE_CALLBACK. +import { EventEmitter } from "node:events"; +import { Duplex, Readable, Transform, Writable } from "node:stream"; + +const log: string[] = []; + +function ticks(count: number): void { + let i = 0; + const step = (): void => { + log.push("t" + i); + if (++i < count) process.nextTick(step); + }; + process.nextTick(step); +} + +class Store extends Writable { + private ready = false; + _construct(callback: (error?: Error | null) => void): void { + log.push("store construct"); + setTimeout(() => { + this.ready = true; + log.push("store ready"); + callback(); + }, 2); + } + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + log.push(`store write ${chunk.toString()} ready=${this.ready}`); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + log.push("store final"); + cb(); + } +} + +class Pair extends Duplex { + constructor() { + super({ readableObjectMode: true }); + } + _construct(callback: (error?: Error | null) => void): void { + log.push("pair construct"); + process.nextTick(callback); + } + _read(): void {} + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + this.push({ got: chunk.toString() }); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + this.push(null); + cb(); + } +} + +function closed(stream: EventEmitter): Promise { + return new Promise((resolve) => stream.on("close", () => resolve())); +} + +async function main(): Promise { + ticks(4); + const store = new Store(); + log.push("write " + store.write("a")); + store.end("b"); + store.on("finish", () => log.push("store finish")); + await closed(store); + + const source = new Readable({ + construct(cb: (error?: Error | null) => void) { + log.push("source construct"); + process.nextTick(cb); + }, + read() { + log.push("source read"); + this.push("x"); + this.push(null); + }, + }); + source.on("data", (c: Buffer) => log.push("source data " + c.toString())); + await closed(source); + + const pair = new Pair(); + pair.on("data", (v: { got: string }) => log.push("pair data " + v.got)); + pair.write("p"); + pair.end(); + await closed(pair); + + const upper = new Transform({ + construct(cb: (error?: Error | null) => void) { + log.push("upper construct"); + cb(); + }, + transform(c: Buffer, _e: string, cb: (error: Error | null, data?: Buffer) => void) { + cb(null, Buffer.from(c.toString().toUpperCase())); + }, + }); + upper.on("data", (c: Buffer) => log.push("upper " + c.toString())); + upper.end("shout"); + await closed(upper); + + const failing = new Writable({ + construct(cb: (error?: Error | null) => void) { + cb(new Error("no target")); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + failing.on("error", (e: Error) => log.push("failing " + e.message)); + await closed(failing); + + const raced = new Writable({ + construct(cb: (error?: Error | null) => void) { + setTimeout(() => cb(new Error("late")), 2); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + destroy(err: Error | null, cb: (error?: Error | null) => void) { + log.push("raced destroy " + (err ? err.message : "none")); + cb(err); + }, + }); + raced.on("error", (e: Error) => log.push(`raced ${e.name} ${e.message}`)); + raced.destroy(new Error("early")); + await closed(raced); + + const thrown = new Writable({ + construct() { + throw new Error("thrown in construct"); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + thrown.on("error", (e: Error) => log.push("thrown " + e.message)); + await closed(thrown); + + const twice = new Writable({ + construct(cb: (error?: Error | null) => void) { + cb(); + cb(); + }, + write(_c: Buffer, _e: string, cb: (error?: Error | null) => void) { + cb(); + }, + }); + twice.on("error", (e: Error) => log.push("twice " + (e as NodeJS.ErrnoException).code)); + await closed(twice); + console.log(log.join("\n")); +} +main(); diff --git a/tests/corpus/7247-stream-construct.mjs b/tests/corpus/7247-stream-construct.mjs new file mode 100644 index 000000000..355116fa7 --- /dev/null +++ b/tests/corpus/7247-stream-construct.mjs @@ -0,0 +1,101 @@ +// Deferred construction in JavaScript: construct in an options literal, +// in an options value read at run time, and _construct on a constructor +// function's prototype, with the write, read, error, destroy-while- +// constructing, throwing and double-callback paths. +import * as stream from "node:stream"; +import { Writable, Readable } from "node:stream"; +import * as util from "node:util"; +const log = []; +const tick = (n) => { let i = 0; const f = () => { log.push("t" + i); if (++i < n) process.nextTick(f); }; process.nextTick(f); }; +tick(4); +const wopts = { + construct(cb) { log.push("construct"); setTimeout(() => { log.push("constructed"); cb(); }, 2); }, + write(c, e, cb) { log.push("write " + c + "/" + e); cb(); }, + final(cb) { log.push("final"); cb(); }, +}; +const w = new Writable(dyn(wopts)); +log.push("write ret " + w.write("a")); +w.end("b"); +w.on("finish", () => log.push("finish")); +w.on("close", () => { log.push("close"); next(); }); +function next() { + const ropts = { construct(cb) { log.push("r construct"); process.nextTick(cb); }, read() { log.push("read"); r.push("x"); r.push(null); } }; + const r = new Readable(dyn(ropts)); + r.on("data", (c) => log.push("data " + c)); + r.on("end", () => { log.push("r end"); errs(); }); +} +function errs() { + const eo = { construct(cb) { cb(new Error("boom")); }, write(c, e2, cb) { cb(); } }; + const e = new Writable(dyn(eo)); + e.on("error", (err) => log.push("error " + err.message)); + e.on("close", () => { log.push("e close"); destroyDuring(); }); +} +function destroyDuring() { + const dopts = { construct(cb) { setTimeout(() => cb(new Error("late")), 2); }, write(c, e, cb) { cb(); }, destroy(err, cb) { log.push("destroy " + (err && err.message)); cb(err); } }; + const d = new Writable(dyn(dopts)); + d.on("error", (err) => log.push("d error " + err.name + " " + err.message + " " + (err.errors ? err.errors.map((x) => x.message).join("+") : ""))); + d.on("close", () => { log.push("d close"); throwing(); }); + d.destroy(new Error("early")); +} +function throwing() { + const to = { construct(cb) { throw new Error("thrown"); }, write(c, e, cb) { cb(); } }; + const t = new Writable(dyn(to)); + t.on("error", (err) => log.push("t error " + err.message)); + t.on("close", () => { log.push("t close"); twice(); }); +} +function twice() { + const o = { construct(cb) { cb(); cb(); }, write(c, e, cb) { cb(); } }; + const t = new Writable(dyn(o)); + t.on("error", (err) => log.push("twice error " + err.code)); + t.on("close", () => { log.push("twice close"); viaLiteral(); }); + setTimeout(() => t.destroy(), 5); +} +function dyn(o) { + return o; +} +function viaLiteral() { + const lit = new Writable({ + construct(cb) { + log.push("literal construct " + (this === lit)); + setImmediate(cb); + }, + write(chunk, enc, cb) { + log.push(`literal write ${chunk}/${enc}`); + cb(); + }, + }); + lit.end("z", () => { + log.push("literal end"); + viaConstructor(); + }); +} +function viaConstructor() { + function Bad() { + stream.Readable.call(this); + } + util.inherits(Bad, stream.Readable); + Bad.prototype._construct = function () { + throw new Error("ctor threw"); + }; + Bad.prototype._read = function () {}; + function Late(opts) { + stream.Writable.call(this, opts); + } + util.inherits(Late, stream.Writable); + Late.prototype._construct = function (cb) { + log.push("late construct " + (this instanceof Late)); + setTimeout(cb, 1); + }; + Late.prototype._write = function (chunk, enc, cb) { + log.push("late write " + chunk); + cb(); + }; + const late = new Late(); + late.write("q"); + late.end(() => { + log.push("late end"); + const bad = new Bad(); + bad.on("error", (e) => log.push("bad " + e.message)); + bad.on("close", () => console.log(log.join("\n"))); + }); +} diff --git a/tests/corpus/7248-stream-writev.ts b/tests/corpus/7248-stream-writev.ts new file mode 100644 index 000000000..2df298d6d --- /dev/null +++ b/tests/corpus/7248-stream-writev.ts @@ -0,0 +1,124 @@ +// Batched writes in typed code: the writev option and the _writev method +// on Writable and Duplex. Writes buffered behind a write in flight or a +// cork reach writev as one [{ chunk, encoding }] batch whose callback +// runs the writes' callbacks in order; a stream with only writev sends +// each lone write as a batch of one; a later `stream._writev = fn` +// takes over; an object-mode side batches its values. +import { EventEmitter } from "node:events"; +import { Duplex, Writable } from "node:stream"; + +interface Entry { + chunk: Buffer; + encoding: string; +} + +interface ValueEntry { + chunk: unknown; + encoding: string | undefined; +} + +const log: string[] = []; + +function finished(stream: EventEmitter): Promise { + return new Promise((resolve) => stream.on("finish", () => resolve())); +} + +class Batcher extends Writable { + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + log.push("batcher write " + chunk.toString()); + setTimeout(cb, 1); + } + _writev(chunks: Entry[], cb: (error?: Error | null) => void): void { + log.push(`batcher writev ${chunks.map((c) => c.chunk.toString() + "/" + c.encoding).join(",")}`); + setTimeout(cb, 1); + } +} + +class Echo extends Duplex { + _read(): void {} + _write(chunk: Buffer, _enc: string, cb: (error?: Error | null) => void): void { + this.push(chunk); + cb(); + } + _writev(chunks: Entry[], cb: (error?: Error | null) => void): void { + this.push(Buffer.concat(chunks.map((c) => c.chunk))); + cb(); + } + _final(cb: (error?: Error | null) => void): void { + this.push(null); + cb(); + } +} + +async function main(): Promise { + const batcher = new Batcher(); + batcher.write("a", () => log.push("a done")); + batcher.write("b", () => log.push("b done")); + batcher.write("c"); + batcher.write(Buffer.from("d"), () => log.push("d done")); + batcher.end(() => log.push("batcher end")); + await finished(batcher); + + const corked = new Writable({ + writev(chunks: Entry[], cb: (error?: Error | null) => void) { + log.push(`corked ${chunks.length}: ${chunks.map((c) => c.chunk.toString("hex")).join(" ")}`); + cb(); + }, + }); + corked.cork(); + corked.write("x"); + corked.write("ff", "hex"); + corked.write("y"); + corked.uncork(); + corked.write("z", () => log.push("z done")); + corked.end(); + await finished(corked); + + const late = new Writable({ + write(c: Buffer, _e: string, cb: () => void) { + log.push("late write " + c.toString()); + cb(); + }, + }); + late._writev = (chunks: Entry[], cb: (error?: Error | null) => void) => { + log.push("late writev " + chunks.length); + cb(); + }; + late.cork(); + late.write("1"); + late.write("2"); + late.uncork(); + late.end(); + await finished(late); + + const echo = new Echo(); + const echoed: string[] = []; + echo.on("data", (c: Buffer) => echoed.push(c.toString())); + echo.cork(); + echo.write("e1"); + echo.write("e2"); + process.nextTick(() => { + echo.uncork(); + echo.end("e3"); + }); + await new Promise((resolve) => echo.on("end", resolve)); + log.push("echoed " + echoed.join(",")); + + const values = new Writable({ + objectMode: true, + writev(chunks: ValueEntry[], cb: (error?: Error | null) => void) { + log.push( + "values " + chunks.map((c) => `${JSON.stringify(c.chunk)}/${String(c.encoding)}`).join(","), + ); + cb(); + }, + }); + values.cork(); + values.write({ n: 1 }); + values.write([2], "utf8"); + values.uncork(); + values.end(); + await finished(values); + console.log(log.join("\n")); +} +main(); diff --git a/tests/corpus/7249-stream-writev.cjs b/tests/corpus/7249-stream-writev.cjs new file mode 100644 index 000000000..0e41fe1c8 --- /dev/null +++ b/tests/corpus/7249-stream-writev.cjs @@ -0,0 +1,89 @@ +// Batched writes in JavaScript: the writev option in a literal and in an +// options value, and _writev on a constructor function's prototype. The +// batch entries carry chunk, encoding and callback (Node's nop when the +// write passed none), allBuffers says whether every chunk is a Buffer, +// and a stream with only _writev sends a lone write as { chunk, encoding }. +const stream = require("node:stream"); +const util = require("node:util"); + +const log = []; +const describe = (chunks) => + chunks + .map((c) => { + const chunk = Buffer.isBuffer(c.chunk) ? "B:" + c.chunk.toString() : `${typeof c.chunk}:${c.chunk}`; + const callback = typeof c.callback === "function" ? c.callback.name || "fn" : String(c.callback); + return `${chunk}/${c.encoding}/${callback}`; + }) + .join(",") + + ` all=${chunks.allBuffers} keys=${Object.keys(chunks[0]).join(".")}`; + +function viaLiteral(next) { + const w = new stream.Writable({ + write(chunk, enc, cb) { + log.push("write " + chunk); + setTimeout(cb, 1); + }, + writev(chunks, cb) { + log.push("writev " + describe(chunks)); + setTimeout(cb, 1); + }, + }); + w.write("a", () => log.push("cb a")); + w.write("b", () => log.push("cb b")); + w.write("c"); + w.write(Buffer.from("d"), () => log.push("cb d")); + w.end(() => log.push("end")); + w.on("finish", () => { + log.push("finish"); + next(); + }); +} + +function dyn(value) { + return value; +} + +function viaValue(next) { + const options = { + writev(chunks, cb) { + log.push("value " + describe(chunks)); + cb(); + }, + }; + const v = new stream.Writable(dyn(options)); + v.cork(); + v.write("x"); + v.write("6869", "hex"); + v.uncork(); + v.write("z", () => log.push("z cb")); + v.end(); + v.on("finish", () => { + log.push("value finish"); + next(); + }); +} + +function Sink(opts) { + stream.Writable.call(this, opts); +} +util.inherits(Sink, stream.Writable); +Sink.prototype._writev = function (chunks, cb) { + log.push("sink " + (this instanceof Sink) + " " + describe(chunks)); + cb(); +}; + +function viaConstructor(next) { + const s = new Sink({ decodeStrings: false }); + s.cork(); + s.write("a"); + s.write("6869", "hex"); + s.write(Buffer.from("b")); + s.uncork(); + s.write("lone"); + s.end(() => { + log.push("sink end"); + next(); + }); +} + +viaLiteral(() => viaValue(() => viaConstructor(() => console.log(log.join("\n"))))); diff --git a/tests/corpus/7250-stream-decode-strings.ts b/tests/corpus/7250-stream-decode-strings.ts new file mode 100644 index 000000000..57e84aff2 --- /dev/null +++ b/tests/corpus/7250-stream-decode-strings.ts @@ -0,0 +1,89 @@ +// decodeStrings: false and the write-side default encoding in typed code: +// a string write reaches _write and _transform as the string with its +// encoding (its UTF-16 length counts toward writableLength), Buffers keep +// 'buffer', defaultEncoding decides how write(string) encodes (and the +// name _write sees when strings stay strings), and setDefaultEncoding() +// lowercases, validates and chains. +import { PassThrough, Transform, Writable } from "node:stream"; + +const log: string[] = []; + +function describe(chunk: string | Buffer, enc: string): string { + return typeof chunk === "string" ? `string:${chunk}/${enc}` : `B:${chunk.toString("hex")}/${enc}`; +} + +class Keeper extends Writable { + seen: string[] = []; + constructor() { + super({ decodeStrings: false, highWaterMark: 4 }); + } + _write(chunk: string | Buffer, enc: string, cb: (error?: Error | null) => void): void { + this.seen.push(describe(chunk, enc)); + setTimeout(cb, 1); + } +} + +async function main(): Promise { + const keeper = new Keeper(); + log.push("ret " + keeper.write("héllo")); + log.push("length " + keeper.writableLength); + keeper.write("6869", "hex"); + keeper.write(Buffer.from("zz")); + keeper.end("end", "latin1"); + await new Promise((resolve) => keeper.on("finish", resolve)); + log.push("keeper " + keeper.seen.join(" ")); + + const hex = new Writable({ + defaultEncoding: "hex", + write(chunk: Buffer, enc: string, cb: (error?: Error | null) => void) { + log.push(`hex ${chunk.toString()} ${enc}`); + cb(); + }, + }); + hex.write("6869"); + hex.write("41", "utf8"); + hex.setDefaultEncoding("BASE64").write("aGk="); + try { + hex.setDefaultEncoding("nope"); + } catch (e) { + log.push(`setDefaultEncoding ${(e as NodeJS.ErrnoException).code} ${(e as Error).message}`); + } + log.push("chains " + (hex.setDefaultEncoding("utf8") === hex)); + hex.end("ok"); + + const both = new Writable({ + decodeStrings: false, + defaultEncoding: "LATIN1", + write(chunk: string | Buffer, enc: string, cb: (error?: Error | null) => void) { + log.push("both " + describe(chunk, enc)); + cb(); + }, + }); + both.write("x"); + both.setDefaultEncoding("UTF-8"); + both.write("y"); + both.end(); + + const t = new Transform({ + decodeStrings: false, + transform(chunk: string | Buffer, enc: string, cb: (error: Error | null, data?: string | Buffer) => void) { + log.push("transform " + describe(chunk, enc)); + cb(null, chunk); + }, + }); + t.on("data", (c: Buffer) => log.push("transform out " + c.toString("hex"))); + t.write("abc"); + t.end("ff", "hex"); + + const p = new PassThrough({ decodeStrings: false }); + p.on("data", (c: Buffer) => log.push("pass " + c.toString())); + p.end("q"); + + try { + new Writable({ defaultEncoding: "bogus" }); + } catch (e) { + log.push("constructor " + (e as NodeJS.ErrnoException).code); + } + setTimeout(() => console.log(log.join("\n")), 10); +} +main(); diff --git a/tests/corpus/7251-stream-write-encodings.mjs b/tests/corpus/7251-stream-write-encodings.mjs new file mode 100644 index 000000000..5972f6684 --- /dev/null +++ b/tests/corpus/7251-stream-write-encodings.mjs @@ -0,0 +1,65 @@ +// Write encodings in JavaScript: decodeStrings: false and defaultEncoding +// in options literals and options values, setDefaultEncoding(), and +// strings written in other encodings to constructor-function streams. +import * as stream from "node:stream"; +import { PassThrough, Transform, Writable } from "node:stream"; +import * as util from "node:util"; + +const log = []; +const describe = (c, e) => `${Buffer.isBuffer(c) ? "B:" + c.toString("hex") : typeof c + ":" + c}/${e}`; +const dyn = (value) => value; + +const keep = new Writable({ decodeStrings: false, highWaterMark: 4, write(c, e, cb) { log.push("keep " + describe(c, e)); cb(); } }); +log.push("ret " + keep.write("héllo") + " length " + keep.writableLength); +keep.write("6869", "hex"); +keep.write(Buffer.from("zz")); +keep.end("end", "latin1"); + +const valueOpts = { decodeStrings: false, defaultEncoding: "HEX", write(c, e, cb) { log.push("value " + describe(c, e)); cb(); } }; +const fromValue = new Writable(dyn(valueOpts)); +fromValue.write("ab"); +fromValue.write(Buffer.from("c")); + +const hex = new Writable({ defaultEncoding: "hex", write(c, e, cb) { log.push("hex " + describe(c, e)); cb(); } }); +hex.write("6869"); +hex.write("41", "utf8"); +hex.setDefaultEncoding("BASE64"); +hex.write("aGk="); +try { + hex.setDefaultEncoding("nope"); +} catch (e) { + log.push("setDefaultEncoding " + e.code + " " + e.message); +} +log.push("chains " + (hex.setDefaultEncoding("utf8") === hex)); + +const t = new Transform({ decodeStrings: false, transform(c, e, cb) { log.push("t " + describe(c, e)); cb(null, c); } }); +t.on("data", (c) => log.push("t data " + describe(c, "-"))); +t.write("abc"); +t.write("ff", "hex"); +const p = new PassThrough({ decodeStrings: false }); +p.on("data", (c) => log.push("p data " + describe(c, "-"))); +p.write("q"); + +const om = new Writable({ objectMode: true, defaultEncoding: "hex", write(c, e, cb) { log.push("om " + describe(c, e)); cb(); } }); +om.write("s"); +try { + new Writable({ defaultEncoding: "bogus" }); +} catch (e) { + log.push("constructor " + e.code); +} + +function Sink() { + stream.Writable.call(this); +} +util.inherits(Sink, stream.Writable); +Sink.prototype._write = function (c, e, cb) { + log.push("sink " + describe(c, e)); + cb(); +}; +const sink = new Sink(); +sink.write("6869", "hex"); +sink.setDefaultEncoding("latin1"); +sink.write("é"); +sink.end("YQ==", "base64"); + +setTimeout(() => console.log(log.join("\n")), 5); diff --git a/tests/harness/corpus-costs/7246-stream-construct.ts.json b/tests/harness/corpus-costs/7246-stream-construct.ts.json new file mode 100644 index 000000000..d94320c7e --- /dev/null +++ b/tests/harness/corpus-costs/7246-stream-construct.ts.json @@ -0,0 +1 @@ +{ "plain": 2327, "san": 3064 } diff --git a/tests/harness/corpus-costs/7247-stream-construct.mjs.json b/tests/harness/corpus-costs/7247-stream-construct.mjs.json new file mode 100644 index 000000000..a74da86c8 --- /dev/null +++ b/tests/harness/corpus-costs/7247-stream-construct.mjs.json @@ -0,0 +1 @@ +{ "plain": 2834, "san": 3538 } diff --git a/tests/harness/corpus-costs/7248-stream-writev.ts.json b/tests/harness/corpus-costs/7248-stream-writev.ts.json new file mode 100644 index 000000000..83ccdb03d --- /dev/null +++ b/tests/harness/corpus-costs/7248-stream-writev.ts.json @@ -0,0 +1 @@ +{ "plain": 2294, "san": 3026 } diff --git a/tests/harness/corpus-costs/7249-stream-writev.cjs.json b/tests/harness/corpus-costs/7249-stream-writev.cjs.json new file mode 100644 index 000000000..2fc75720e --- /dev/null +++ b/tests/harness/corpus-costs/7249-stream-writev.cjs.json @@ -0,0 +1 @@ +{ "plain": 2640, "san": 2765 } diff --git a/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json b/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json new file mode 100644 index 000000000..b6c0a39a0 --- /dev/null +++ b/tests/harness/corpus-costs/7250-stream-decode-strings.ts.json @@ -0,0 +1 @@ +{ "plain": 2016, "san": 2519 } diff --git a/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json b/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json new file mode 100644 index 000000000..7e459d978 --- /dev/null +++ b/tests/harness/corpus-costs/7251-stream-write-encodings.mjs.json @@ -0,0 +1 @@ +{ "plain": 2161, "san": 2661 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 176caa516..c0176eb93 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 4e06e72b75fbc1d2095291cf38df48fc46515594 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:54:40 -0500 Subject: [PATCH 022/169] Key flatMap union helpers by their arm plan and copy array elements as stored The union-returning flatMap helper was interned by element and result types alone, so two callbacks with one union result type, one with an empty `never[]` arm and one with a populated array arm, shared the first helper and the second dropped its elements. The key now includes the per-arm plan. Array arms copy their elements as stored, a present undefined included, so an array arm's element type must be the result's, and a unit-typed result (`undefined`) appends itself. --- .../lowering/containers/array-callbacks.ts | 34 +++++++++++++------ tests/corpus/7029-flatmap-union-results.ts | 13 +++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 3de1a4e6b..02d0bc3e7 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1588,9 +1588,10 @@ interface FlatMapUnionPlan { arms: ("spread" | "empty" | "value")[]; } -/** The plan for a callback returning `T | U[]`: every non-array arm and - * every array arm's element must convert into the result's element type - * without a check. Null when an arm does not. */ +/** The plan for a callback returning `T | U[]`: every non-array arm must + * convert into the result's element type without a check, and every + * array arm's element type must be the result's. Null when an arm does + * not. */ function flatMapUnionPlan( lowerer: Lowerer, call: ts.CallExpression, @@ -1623,7 +1624,9 @@ function flatMapUnionPlan( const plan: FlatMapUnionPlan["arms"] = []; for (const arm of arms) { if (arm.kind === "array" && emptyArms.some((e) => typeEquals(e, arm))) plan.push("empty"); - else if (arm.kind === "array" && converts(arm.elem)) plan.push("spread"); + // An array arm copies its elements as they are stored, a present + // undefined included, so its element type is the result's. + else if (arm.kind === "array" && typeEquals(arm.elem, outElem)) plan.push("spread"); else if (arm.kind !== "array" && converts(arm)) plan.push("value"); else return null; } @@ -1642,7 +1645,9 @@ function flatMapUnionHelper( loc: SrcLoc, present: boolean, ): string { - const key = `flatMapUnion:${typeKey(elem)}:${typeKey(fnRet)}:${typeKey(plan.outElem)}:${arity}${present ? ":present" : ""}`; + // The plan is part of the key: one union result type can carry an empty + // `never[]` arm in one callback and a populated array arm in another. + const key = `flatMapUnion:${typeKey(elem)}:${typeKey(fnRet)}:${typeKey(plan.outElem)}:${plan.arms.join(",")}:${arity}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; @@ -1683,7 +1688,19 @@ function flatMapUnionHelper( lowerer, outRef(), outLength(), - lowerer.coerceToExpected(narrowed, plan.outElem), + lowerer.coerceToExpected( + // A unit arm's value is the unit itself, which has no + // narrowed representation. + isUnitType(arm) + ? { + kind: "unitLit", + unit: arm.kind === "nullT" ? "null" : "undefined", + type: arm, + loc, + } + : narrowed, + plan.outElem, + ), plan.outElem, loc, ), @@ -1749,10 +1766,7 @@ function flatMapUnionHelper( lowerer, outRef(), outLength(), - lowerer.coerceToExpected( - varRef(itemId, arrayValueType(lowerer, innerT.elem), loc), - plan.outElem, - ), + varRef(itemId, arrayValueType(lowerer, innerT.elem), loc), plan.outElem, loc, ), diff --git a/tests/corpus/7029-flatmap-union-results.ts b/tests/corpus/7029-flatmap-union-results.ts index 83c9583de..efeae4d8f 100644 --- a/tests/corpus/7029-flatmap-union-results.ts +++ b/tests/corpus/7029-flatmap-union-results.ts @@ -29,3 +29,16 @@ const out = words.flatMap((s) => { return s.length > 0 ? s.toUpperCase() : []; }); console.log(out, calls); + +// One union result type with an empty arm in one callback and a populated +// array arm in another. +const counts = [1, 2, 3]; +console.log(counts.flatMap((n) => (n % 2 ? n : [])), counts.flatMap((n) => (n % 2 ? n : [n, n]))); + +// Array results keep present undefined elements, and an undefined result +// appends itself. +const withGaps: (number | undefined)[] = [4, undefined, 6]; +const maybe: (number | undefined)[] = [1, 2, undefined]; +console.log(maybe.flatMap((n) => (n === 2 ? withGaps : n))); +const gaps: (string | undefined)[] = ["x", undefined]; +console.log(["a", "b"].flatMap((s) => (s === "b" ? gaps : s))); From 0078dc2c46c93e6443fae1e3296456129ba190e0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:10:59 -0500 Subject: [PATCH 023/169] Check the elements a spread push stores into an evolving empty array The evolving-array check made the binding checked-dynamic for any spread write, so `xs.push(...values)` of an array whose elements match the binding's agreed element type turned a typed `number[]` into a checked-dynamic array, and the spread call through it was refused with SC1090 "spread arguments in calls through 'unknown' values". A spread now stores the source array's element type, or each of a tuple's element types, and the binding stays typed when they match. A spread of another iterable still makes it checked-dynamic. --- .../src/frontend/lowering/lower-stmts.ts | 27 ++++++++++++++----- tests/corpus/7006-evolving-array-aliases.ts | 9 +++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 29086d326..cc01d7401 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -4800,9 +4800,9 @@ export function lowerVarDeclList( * it, and reads inside closures as the unsettled array. When every read * with a settled element type maps to the same array type, the binding has * that type. When reads disagree, a write (`xs.push(v)`, `xs.unshift(v)`, - * `xs[i] = v`) stores a value that type's element does not hold, or the - * binding is reassigned, it is checked-dynamic, as the same binding is at - * module scope. Null for other bindings. */ + * `xs.push(...values)`, `xs[i] = v`) stores a value that type's element does + * not hold, or the binding is reassigned, it is checked-dynamic, as the same + * binding is at module scope. Null for other bindings. */ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; let initializer: ts.Expression = decl.initializer; @@ -4870,11 +4870,24 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy result = mapped; } if (result === null || result.kind !== "array") return DYN; + // The element types a write stores: its value's, or for a spread + // (`xs.push(...values)`) the source array's element or tuple's elements. + // Another spread source stores elements this check cannot see. + const storedTypes = (value: ts.Expression): ts.Type[] | null => { + if (!ts.isSpreadElement(value)) return [lowerer.typeOf(value)]; + const source = lowerer.typeOf(value.expression); + if (lowerer.checker.isArrayType(source) || lowerer.checker.isTupleType(source)) + return [...lowerer.checker.getTypeArguments(source as ts.TypeReference)]; + return null; + }; for (const value of written) { - if (ts.isSpreadElement(value)) return DYN; - const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(value))); - if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null)) - return DYN; + const stored = storedTypes(value); + if (stored === null) return DYN; + for (const type of stored) { + const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(type)); + if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null)) + return DYN; + } } return result; } diff --git a/tests/corpus/7006-evolving-array-aliases.ts b/tests/corpus/7006-evolving-array-aliases.ts index 4810c440e..215b3f3c5 100644 --- a/tests/corpus/7006-evolving-array-aliases.ts +++ b/tests/corpus/7006-evolving-array-aliases.ts @@ -68,3 +68,12 @@ console.log(chain()); console.log(viaAlias(), "|", letAlias()); console.log(nested(true), "|", nested(false), "|", elementWrite()); console.log(agreeing(), "|", passed()); + +function spreadSame(): number { + const xs = []; + xs.push(1); + const more = [2, 3]; + xs.push(...more); + return xs.reduce((sum, v) => sum + v, 0); +} +console.log(spreadSame()); From 6e79566c255877f002203ac0eeeac0cd1928e9a4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:18:57 -0500 Subject: [PATCH 024/169] Keep key order and absent optional fields in filter predicate record copies A record conversion behind a written type predicate built the copy in the target shape's field order and wrote every optional field, so a source object without an optional key came out with that key present and holding undefined, and Object.keys listed the target's order. The copy now keeps the source's key order, as a width copy does, and an optional field the source lacks stays absent. --- .../lowering/containers/filter-predicates.ts | 34 +++++++++++++++++-- .../7007-filter-written-type-predicates.ts | 21 ++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts index df57813fe..3934edbf0 100644 --- a/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts +++ b/packages/compiler/src/frontend/lowering/containers/filter-predicates.ts @@ -176,13 +176,43 @@ function conversionBody( }; const value = converted(lowerer, read, own.type, field.type, loc); if (value === null) return null; - fields.push({ name: field.name, value }); + fields.push({ + name: field.name, + value: keepAbsence(lowerer, input, from.shapeId, field, value, loc), + }); } - return [{ kind: "return", value: { kind: "recordLit", fields, type: to, loc }, loc }]; + // The copy keeps the source's own keys in the source's order, as a width + // copy does (key-order-witness.ts). + const copy: IrExpr = { kind: "recordLit", fields, type: to, loc }; + return [{ kind: "return", value: lowerer.keyOrder.copy(copy, from.shapeId), loc }]; } return null; } +/** A converted field value that keeps the source field's absence: an + * optional field the source object lacks stays absent in the copy rather + * than becoming a property that holds undefined. */ +function keepAbsence( + lowerer: Lowerer, + input: IrExpr, + shapeId: string, + field: { name: string; type: IrType }, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + if (field.type.kind !== "union") return value; + if (!lowerer.unions.get(field.type.unionId)?.arms.some((arm) => arm.kind === "undefinedT")) + return value; + return { + kind: "ternary", + cond: { kind: "recordHas", obj: input, shapeId, field: field.name, type: BOOL, loc }, + then: value, + else_: { kind: "fieldAbsent", unionId: field.type.unionId, type: field.type, loc }, + type: field.type, + loc, + }; +} + /** A union value to one of its arms, or to a union of some of its arms: * each arm the target holds returns, any other arm throws. */ function unionConversion( diff --git a/tests/corpus/7007-filter-written-type-predicates.ts b/tests/corpus/7007-filter-written-type-predicates.ts index c2449b469..b75b5727d 100644 --- a/tests/corpus/7007-filter-written-type-predicates.ts +++ b/tests/corpus/7007-filter-written-type-predicates.ts @@ -85,3 +85,24 @@ function isText(v: string | undefined): v is string { } const maybe: (string | undefined)[] = ["a", undefined, "b"]; console.log(maybe.filter(isText).join("-")); + +// Narrowing a record field keeps each object's own keys, in its own order, +// and an optional field the object lacks stays absent. +interface Row { + id: unknown; + note?: string; + label: string; +} +interface NumericRow { + id: number; + note?: string; + label: string; +} +const tableRows: Row[] = [ + { label: "a", id: 1 }, + { label: "b", id: "s" }, + { label: "c", id: 3, note: "n" }, +]; +for (const row of tableRows.filter((r): r is NumericRow => typeof r.id === "number")) { + console.log(Object.keys(row).join(","), JSON.stringify(row), "note" in row, row.id + 1); +} From 8e8abbe1535e6cf808ad334ce13a49d7e7ab0308 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:21:53 -0500 Subject: [PATCH 025/169] Rebuild after edits to source text the output carries --- .../src/frontend/input-tracker.test.ts | 22 +++++++++++++ .../compiler/src/frontend/input-tracker.ts | 31 ++++++++++++++++--- .../src/frontend/lowering/divergences.ts | 5 ++- .../src/frontend/token-equivalence.test.ts | 12 +++++++ .../src/frontend/token-equivalence.ts | 28 +++++++++++------ .../compiler/src/frontend/ts7/ast-node.ts | 23 +++++++++++++- packages/compiler/src/native/prepare.test.ts | 30 ++++++++++++++++++ packages/compiler/src/native/prepare.ts | 5 ++- 8 files changed, 140 insertions(+), 16 deletions(-) diff --git a/packages/compiler/src/frontend/input-tracker.test.ts b/packages/compiler/src/frontend/input-tracker.test.ts index 4b6ef6d90..b4f3957f5 100644 --- a/packages/compiler/src/frontend/input-tracker.test.ts +++ b/packages/compiler/src/frontend/input-tracker.test.ts @@ -60,6 +60,28 @@ test("semantic validation reads non-source dependencies once and still invalidat expect(reads.mock.calls.filter(([path]) => path === dependency)).toHaveLength(1); }); +test("an equivalent edit of a file whose raw text the frontend read does not match", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); + scratch.push(dir); + const source = join(dir, "entry.ts"); + const previous = "export const answer = 1;\n"; + await writeFile(source, previous); + const tracker = new FrontendInputTracker(); + tracker.run(() => { + trackedReadFile(source); + tracker.record({ op: "text", path: source }); + }); + const snapshot = tracker.snapshot(); + expect(validFrontendInputSnapshot(snapshot)).toBe(true); + expect(frontendInputsStillMatch(snapshot)).toBe(true); + await writeFile(source, `// comment\n${previous}`); + const equivalent = vi.fn(() => true); + expect( + frontendInputsSemanticallyMatch(snapshot, new Map([[source, previous]]), equivalent), + ).toBeNull(); + expect(equivalent).not.toHaveBeenCalled(); +}); + test("tracked frontend reads invalidate on byte edits", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-")); scratch.push(dir); diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts index ed2f7b176..d27ab6285 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { AsyncLocalStorage } from "node:async_hooks"; import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; import { basename, dirname, resolve } from "node:path"; +import { observeSourceText } from "./ts7/ast-node.js"; /** * Filesystem observations made while constructing and lowering one frontend @@ -16,7 +17,10 @@ export type FrontendInputProbe = | { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" } | { op: "entries"; path: string; files: string[]; directories: string[] } | { op: "entries-error"; path: string } - | { op: "realpath"; path: string; target: string | null }; + | { op: "realpath"; path: string; target: string | null } + /** The frontend's output depends on the file's raw text or line + * positions, not only on its tokens (observeSourceText). */ + | { op: "text"; path: string }; export interface FrontendInputSnapshot { version: 1; @@ -105,6 +109,7 @@ export class FrontendInputTracker { record(probe: FrontendInputProbe): void { const key = `${probe.op}\0${probe.path}`; + if (probe.op === "text" && this.probes.has(key)) return; const previous = this.probes.get(key); if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) { // An input changed while this frontend was running. The build may @@ -158,6 +163,13 @@ function record(probe: FrontendInputProbe): void { (synchronousTracker ?? activeTracker.getStore())?.record(probe); } +// Source text the frontend reads raw (call-site text in messages, comptime +// callbacks, line numbers) changes with edits that keep the tokens. +observeSourceText((fileName) => { + const tracker = synchronousTracker ?? activeTracker.getStore(); + if (tracker !== undefined) tracker.record({ op: "text", path: resolve(fileName) }); +}); + /** Mark the active frontend as unsafe to persist because an exact host query * bypassed the tracked filesystem wrappers. No-op outside a frontend run. */ export function markFrontendInputsUnstable(): void { @@ -394,6 +406,9 @@ export function frontendInputsStillMatch( return probe.target === null; } } + case "text": + // The file probe of the same path checks its content. + return true; } }); } @@ -411,7 +426,11 @@ export function frontendInputsSemanticallyMatch( ): FrontendSemanticMatch | null { if (!validFrontendInputSnapshot(snapshot)) return null; const sourceProbes = new Map>(); - for (const probe of snapshot.probes) if (probe.op === "file") sourceProbes.set(probe.path, probe); + const rawText = new Set(); + for (const probe of snapshot.probes) { + if (probe.op === "file") sourceProbes.set(probe.path, probe); + else if (probe.op === "text") rawText.add(probe.path); + } for (const [path, previous] of previousSources) { if (sourceProbes.get(path)?.digest !== frontendSourceDigest(previous)) return null; } @@ -436,8 +455,10 @@ export function frontendInputsSemanticallyMatch( probes.push(probe); continue; } - // One edit that is not equivalent already rules out the match. - if (!isSemanticEquivalent(probe.path, previous, current)) return null; + // One edit that is not equivalent already rules out the match, and so + // does any edit of a file whose raw text the frontend read. + if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current)) + return null; changed.push({ path: probe.path, previous, current }); probes.push({ ...probe, digest: currentDigest }); } @@ -488,6 +509,8 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front return true; case "realpath": return value.target === null || typeof value.target === "string"; + case "text": + return true; default: return false; } diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 1c362cb09..c971c672d 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -464,8 +464,11 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null cur = cur.expression; return cur; }; + // The text appears only in warnings. A build that reported warnings is + // not reused after a source edit, so the read is not recorded as a source + // text dependency (ts.getTextOfNode would record it). const textOf = (node: ts.Node): string => { - const text = ts.getTextOfNode(node); + const text = ts.getSourceFileOfNode(node).text.slice(ts.tokenStartOfNode(node), node.end); return text.length > 60 ? `${text.slice(0, 57)}...` : text; }; const visit = (node: ts.Node): "skip" | "stop" | undefined => { diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts index 2c7b8ea0a..afd81d9b6 100644 --- a/packages/compiler/src/frontend/token-equivalence.test.ts +++ b/packages/compiler/src/frontend/token-equivalence.test.ts @@ -28,6 +28,18 @@ test("comment-like text inside regular expressions is code", () => { ); expect(equivalent("const q = a / b; // ratio\n", "const q = a / b; // the ratio\n")).toBe(true); expect(equivalent("return /a/; // one\n", "return /a/; // two\n")).toBe(true); + // A `for await` header closes like a `for` header. + expect( + equivalent( + "for await (const x of xs) /[//]/.test(x); c = 1;\n", + "for await (const x of xs) /[//]/.test(x); c = 2;\n", + ), + ).toBe(false); +}); + +test("U+2028 and U+2029 end a line comment, so their files are never equivalent", () => { + expect(equivalent("// a\u2028c = 1;\n", "// a\u2028c = 2;\n")).toBe(false); + expect(equivalent("/* a\u2029*/ c = 1;\n", "/* b\u2029*/ c = 1;\n")).toBe(false); }); test("comments that can affect compilation are kept", () => { diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts index 6387a0e4b..94c60d836 100644 --- a/packages/compiler/src/frontend/token-equivalence.ts +++ b/packages/compiler/src/frontend/token-equivalence.ts @@ -15,12 +15,14 @@ * annotations lowering reads). * - A `/` starts a regular expression or divides according to the token * before it. After `)` it divides unless the parenthesis closes an `if`, - * `while`, `for`, or `with` header. After `}`, `++` or `--` the scanner + * `while`, `for`, `for await`, or `with` header. After `}`, `++` or `--` the scanner * cannot tell which, so it gives up, as it does for escaped identifiers, * which can spell keywords. * - Template literals track their `${` substitutions on a brace stack. - * - JSX files, unterminated strings, comments, templates and regular - * expressions, and any line terminator outside ASCII give up. + * - JSX files, files that contain U+2028 or U+2029 anywhere (they end a + * line comment and separate lines for automatic semicolon insertion), + * and unterminated strings, comments, templates and regular expressions + * give up. * * Plain character scans keep the scanner free of Unicode property escapes: * any non-ASCII code unit outside strings, templates, regular expressions @@ -118,6 +120,7 @@ function isSpace(code: number): boolean { /** The tokens of `source`, or null when the scanner cannot classify it. */ function scan(path: string, source: string): Token[] | null { if (path.endsWith(".tsx") || path.endsWith(".jsx")) return null; + if (source.includes("\u2028") || source.includes("\u2029")) return null; const typeScript = isTypeScript(path); const tokens: Token[] = []; // Brace kinds: true for a template substitution, false otherwise. @@ -125,13 +128,17 @@ function scan(path: string, source: string): Token[] | null { // Paren kinds: true for a statement header. const parens: boolean[] = []; let lineBreakBefore = false; - // The previous token, and whether a `)` closed a statement header. - const last: { token: string | null } = { token: null }; + // The previous token and the one before it, and whether a `)` closed a + // statement header. + const last: { token: string | null; before: string | null } = { token: null, before: null }; let closedHeader = false; const push = (text: string, comment = false): void => { tokens.push({ text, comment, lineBreakBefore }); lineBreakBefore = false; - if (!comment) last.token = text; + if (!comment) { + last.before = last.token; + last.token = text; + } }; let index = 0; const length = source.length; @@ -162,6 +169,7 @@ function scan(path: string, source: string): Token[] | null { index = position; push(source.slice(start, position)); braces.push(true); + last.before = last.token; last.token = "${"; return true; } @@ -176,8 +184,7 @@ function scan(path: string, source: string): Token[] | null { index++; continue; } - if (code > 127 && (code === 0x2028 || code === 0x2029 || code === 0xa0 || code === 0xfeff)) - return null; + if (code > 127 && (code === 0xa0 || code === 0xfeff)) return null; const next = source.charCodeAt(index + 1); // Comments. if (code === 47 && (next === 47 || next === 42)) { @@ -305,7 +312,10 @@ function scan(path: string, source: string): Token[] | null { } // Brackets. if (code === 40) { - parens.push(last.token !== null && HEADER_KEYWORDS.has(last.token)); + parens.push( + last.token !== null && + (HEADER_KEYWORDS.has(last.token) || (last.token === "await" && last.before === "for")), + ); index++; push("("); closedHeader = false; diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index be2745616..40e77e6f1 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -17,6 +17,16 @@ import { import type { SourceFile } from "./ast-types.js"; import { SyntaxKind, NodeFlags } from "./enums.js"; +/** Receives the file name of a source file whose raw text or line + * positions a reader takes: the text of a node other than a token + * (getText), a node's text with its trivia (getFullText), or line starts. */ +let sourceTextObserver: ((fileName: string) => void) | undefined; + +/** Installs the observer of raw source text reads (sourceTextObserver). */ +export function observeSourceText(observer: ((fileName: string) => void) | undefined): void { + sourceTextObserver = observer; +} + /** A list of child nodes with the list's own position, the shape of tsc-ts's * NodeList. The file keeps the nodes array, not this object: each read of a * list field returns a new AstNodeList over the same array, so `nodes` keeps @@ -202,7 +212,14 @@ export class AstFile { return result; } + /** Reports a read of this file's raw text or line positions. */ + noteSourceText(): void { + if (sourceTextObserver !== undefined && this.root.kind === SyntaxKind.SourceFile) + sourceTextObserver(this.root.fileName); + } + lineStarts(): number[] { + this.noteSourceText(); if (this.lines === undefined) this.lines = astLineStarts(this.root.text ?? ""); return this.lines; } @@ -462,10 +479,14 @@ export class AstNode { return this.getStart(sourceFile) - this.pos; } getFullText(sourceFile?: AstNode): string { - return ((sourceFile ?? this.file.root).text ?? "").substring(this.pos, this.end); + const source = sourceFile ?? this.file.root; + source.file.noteSourceText(); + return (source.text ?? "").substring(this.pos, this.end); } getText(sourceFile?: AstNode): string { const source = sourceFile ?? this.file.root; + // A token's text is the same in every token-equivalent version. + if (this.kind > SyntaxKind.LastToken) source.file.noteSourceText(); return (source.text ?? "").substring(this.getStart(source), this.end); } diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts index 35e866d6a..78d451f46 100644 --- a/packages/compiler/src/native/prepare.test.ts +++ b/packages/compiler/src/native/prepare.test.ts @@ -113,6 +113,36 @@ test("comment and whitespace edits reuse the frontend output a fresh build repro expect(edited.ok && edited.llvm).not.toBe(first.llvm); }); +test("a comment edit inside source text the output carries is rebuilt", () => { + const f = fixture(); + // A failing assert(value) reports its call's source text. + const source = (call: string) => + `import assert from "node:assert";\nexport const value = "first";\n${call};\n`; + writeFileSync(f.dependency, source("assert(value.length > 0)")); + const first = f.prepare(); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + writeFileSync(f.dependency, source("assert(value.length > 0 /* checked */)")); + const edited = f.prepare(); + expect(f.loads()).toBe(2); + expect(edited.ok && edited.llvm).not.toBe(first.llvm); +}); + +test("a whitespace edit that moves a reported source line is rebuilt", () => { + const f = fixture(); + // node:test reports the file, line and column of each test. + const source = (gap: string) => + `import { test } from "node:test";\nexport const value = "first";\n${gap}test("value", () => {});\n`; + writeFileSync(f.dependency, source("")); + const first = f.prepare(); + expect(first.ok, JSON.stringify(first)).toBe(true); + if (!first.ok) return; + writeFileSync(f.dependency, source("\n")); + const edited = f.prepare(); + expect(f.loads()).toBe(2); + expect(edited.ok && edited.llvm).not.toBe(first.llvm); +}); + test("structurally invalid cached features cause a rebuild even with a valid digest", () => { const f = fixture(); expect(f.prepare().ok).toBe(true); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 53b22cfaa..261879194 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -114,7 +114,10 @@ function readFrontendCache( * that cannot affect compilation; see token-equivalence.ts). Equivalent * sources lower to the same IR apart from source locations, which only dev * builds' debug information and the serialized IR carry. Diagnostics carry - * locations too, so an entry that reported warnings is rebuilt. */ + * locations too, so an entry that reported warnings is rebuilt. A changed + * source whose raw text or line positions the frontend read (call-site text + * in messages, comptime callbacks, test locations) is rebuilt as well: + * frontendInputsSemanticallyMatch refuses its `text` probe. */ function equivalentFrontendInput( stale: FrontendCacheEntry, options: CompileRequestOptions, From a8a545a81f9fd6aa5132977d8e26715d4c7df462 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:36:22 -0500 Subject: [PATCH 026/169] Read the characters of ASCII strings in versioned scan loops as bytes `for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++` tested the string's ASCII bit and the index against the length on every character, and its fallback is an opaque runtime call, so LLVM could not hoist either test or vectorize the loop. A typed-array loop version now also covers `s.charCodeAt(index)` of a string binding the loop does not write, at a counter index plus a constant or another index the version proves. A string is immutable, so its bytes, its length and its ASCII bit are fixed when it is built. The guard requires the ASCII bit, so each UTF-16 unit is one byte and the length is the byte length, and the fast copy reads each unit with one byte load. Other strings and indices run the checked loop. --- .../backend/llvm/bytes-loop-versions.test.ts | 69 ++++++++++ .../src/backend/llvm/bytes-loop-versions.ts | 41 ++++-- packages/compiler/src/backend/llvm/emitter.ts | 1 + .../src/backend/llvm/expr-containers.ts | 7 +- .../compiler/src/backend/llvm/string-reads.ts | 38 ++++++ .../7302-string-scan-loop-versions.ts.json | 6 + .../corpus/7302-string-scan-loop-versions.ts | 120 ++++++++++++++++++ .../7302-string-scan-loop-versions.ts.json | 1 + 8 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json create mode 100644 tests/corpus/7302-string-scan-loop-versions.ts create mode 100644 tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index ace8f294c..2e22832a1 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -99,6 +99,7 @@ function match( f: IrFunction, loop: IrStmt & { kind: "for" }, captured: string[] = [], + scalarStrings: string[] = [], ): BytesLoopVersion | null { const ranges = analyzeIntegerRanges(f); const locals = new Map(f.locals.map((l) => [l.id, l])); @@ -111,6 +112,7 @@ function match( proven: new Set(), workers: false, owned: new Set(), + scalarStrings: new Set(scalarStrings), }); } @@ -425,3 +427,70 @@ test("bodies compiled without optimization keep one checked loop", () => { expect(body).not.toContain("bytes.version"); expect(body).toContain("bytes.index.invalid"); }); + +const STR: IrType = { kind: "string" }; +const charCode = (receiver: IrExpr, index: IrExpr): IrExpr => ({ + kind: "strIntrinsic", + method: "charCodeAt", + receiver, + args: [index], + type: F64, + loc, +}); + +/** The kernel with a string parameter `text`. */ +function textKernel(body: IrStmt[], mutable = false): IrFunction { + const kernel = fn(body, [local("text", STR, mutable)]); + kernel.params.push({ localId: "text", name: "text", type: STR }); + return kernel; +} + +test("charCodeAt reads of a string binding version with the typed-array reads", () => { + const read = charCode(ref("text", STR), bin("+", ref("i"), num(1))); + const loop = counted([set(ref("out", f64s), ref("i"), read)]); + const version = match(textKernel([loop]), loop)!; + expect(version.bounds.has(read)).toBe(true); + expect( + version.receivers.map((r) => [r.ref.kind === "varRef" && r.ref.localId, r.string]), + ).toEqual([ + ["out", false], + ["text", true], + ]); + expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([ + [0, 0, 0], + [1, 1, 1], + ]); + // A string the loop rebinds keeps its checked reads. + const rebinding = counted([ + set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i"))), + { kind: "assign", localId: "text", value: ref("text", STR), loc }, + ]); + const rebound = match(textKernel([rebinding], true), rebinding)!; + expect(rebound.receivers.map((r) => r.string)).toEqual([false]); + // A string held as a slice of another string has no header to test. + const sliced = match(textKernel([loop]), loop, [], ["text"])!; + expect(sliced.receivers.map((r) => r.string)).toEqual([false]); + expect(sliced.bounds.has(read)).toBe(false); +}); + +test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => { + const loop = counted([set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i")))]); + const kernel = textKernel([loop]); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m)); + expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/); + const fast = body.slice(body.lastIndexOf("bytes.version.fast")); + expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + expect(fast).not.toContain("@sc_str_char_code_at_int"); + // The checked copy keeps the inline helper and its fallback. + expect(body).toContain("@sc_str_char_code_at_int"); +}); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index c3cee3feb..ad64260a2 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -8,6 +8,7 @@ import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; +import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -45,8 +46,9 @@ export interface BytesLoopVersion { /** Worker programs: typed-array accesses whose views the guard proves * thread-owned (no shared storage). */ owned: ReadonlySet; - /** Root receiver bindings, read once before the loop. */ - receivers: { ref: VarRef; global: boolean }[]; + /** Root receiver bindings, read once before the loop; `string` marks a + * string whose `charCodeAt` reads the version covers. */ + receivers: { ref: VarRef; global: boolean; string: boolean }[]; /** Receivers the guard requires to have no shared storage. */ ownedReceivers: number[]; fixed: FixedCheck[]; @@ -80,6 +82,9 @@ export interface BytesLoopFacts { workers: boolean; /** Accesses already proved thread-owned by an enclosing version. */ owned: ReadonlySet; + /** String bindings held as a slice of another string (string-slices.ts): + * they have no string header for the guard to read. */ + scalarStrings: ReadonlySet; } const MAX_COST = 400; @@ -106,6 +111,12 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * such as `base` for `const base = r * cols`, must also be an exact * integer between -2^32 and 2^32, which the guard tests. * + * `s.charCodeAt(index)` of a string binding qualifies the same way. A + * string is immutable: its bytes, its length and the capacity-word bit that + * proves it ASCII (string-reads.ts) are fixed when it is built. The guard + * also requires that bit, so each UTF-16 unit is one byte and the length + * is the byte length, and the fast copy reads each unit with one byte load. + * * In worker programs, every access of a view that may be shared tests its * shared storage. A view's storage is also fixed at construction, so the * same guard tests each invariant receiver once, and the fast copy reads @@ -196,7 +207,8 @@ export function matchBytesLoopVersion( }; const resolving = new Set(); const root = (e: IrExpr): VarRef | null => { - if (e.kind !== "varRef" || e.type.kind !== "bytes") return null; + if (e.kind !== "varRef" || (e.type.kind !== "bytes" && e.type.kind !== "string")) return null; + if (facts.scalarStrings.has(e.localId)) return null; const init = temp(e.localId); if (init) { if (resolving.has(e.localId)) return null; @@ -337,7 +349,7 @@ export function matchBytesLoopVersion( return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); }, 0); - const receivers: { ref: VarRef; global: boolean }[] = []; + const receivers: { ref: VarRef; global: boolean; string: boolean }[] = []; const receiverIndex = new Map(); const fixed = new Map(); const affineChecks = new Map< @@ -349,7 +361,11 @@ export function matchBytesLoopVersion( if (index === undefined) { index = receivers.length; receiverIndex.set(ref.localId, index); - receivers.push({ ref, global: !facts.locals.has(ref.localId) }); + receivers.push({ + ref, + global: !facts.locals.has(ref.localId), + string: ref.type.kind === "string", + }); } return index; }; @@ -359,7 +375,7 @@ export function matchBytesLoopVersion( const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; - if (facts.workers && !facts.owned.has(node)) { + if (facts.workers && ref.type.kind === "bytes" && !facts.owned.has(node)) { const receiver = receiverOf(ref); const nodes = owned.get(receiver); if (nodes) nodes.push(node); @@ -408,7 +424,8 @@ export function matchBytesLoopVersion( const numeric = byteNumberAccess(e); if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width); else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1); - } + } else if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0]) + access(e, e.receiver, e.args[0], 1); return everyExprChild(e, visitExpr, visitStmt); }; const visitStmt = (s: IrStmt): boolean => { @@ -535,14 +552,20 @@ export function emitBytesLoopVersionGuard( } return value; }); - const lengths = receivers.map((receiver) => { - const len = emitBytesHeaderLength(host, receiver); + const lengths = receivers.map((receiver, index) => { + const len = version.receivers[index]!.string + ? emitStringByteLength(host, receiver) + : emitBytesHeaderLength(host, receiver); if (host.sizeType === "i64") return len; const wide = B.tmp(); B.line(`${wide} = zext ${host.sizeType} ${len} to i64`); return wide; }); const conditions: string[] = []; + // A string's byte length is its UTF-16 length when it is ASCII. + version.receivers.forEach(({ string }, index) => { + if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!)); + }); for (const index of version.ownedReceivers) { const owned = B.tmp(); B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..4933ba8b7 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3869,6 +3869,7 @@ export class LlEmitter { proven: this.bytesBounds, workers: this.mod.workers === true, owned: this.bytesOwned, + scalarStrings: new Set(this.stringSlices.keys()), }); } diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8970aa3be..70e526685 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -49,6 +49,7 @@ import { STRING_CHAR_AT_INT_MARKER, STRING_CHAR_CODE_AT_DECL, STRING_CHAR_CODE_AT_INT_MARKER, + emitAsciiCharCode, STRING_UTF16_LEN_DECL, } from "./string-reads.js"; import { exactInteger, widenInteger } from "./integer-values.js"; @@ -319,8 +320,12 @@ export function emitStrIntrinsic( const t = B.tmp(); const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { - host.declare(STRING_CHAR_CODE_AT_INT_MARKER); const wide = widenInteger(host, index); + // An enclosing loop version proved the string ASCII and the index + // below its length (bytes-loop-versions.ts). + if (host.bytesBounds.has(e)) + return { name: emitAsciiCharCode(host, r.name, wide), type: e.type }; + host.declare(STRING_CHAR_CODE_AT_INT_MARKER); B.line(`${t} = call double @sc_str_char_code_at_int(ptr ${r.name}, i64 ${wide})`); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index 03a9fa282..c5841006c 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -17,6 +17,7 @@ * is thread-local. */ +import type { LlvmEmitterContext } from "./expr-context.js"; import { exactIndexLines, sizeToDouble } from "./common.js"; import { wtf8Bytes } from "../literals.js"; import { literalCapWord, literalCapWord32 } from "./string-key-hash.js"; @@ -37,6 +38,43 @@ export const STRING_CHAR_AT_DECL = "declare ptr @scr_str_char_at(ptr, double)"; /** Requests the integer-index charAt helper, as the marker above does. */ export const STRING_CHAR_AT_INT_MARKER = "; uses @sc_str_char_at_int"; +/** The proven-ASCII flag of string `s`, as an i1. */ +export function emitStringAsciiFlag(host: LlvmEmitterContext, s: string): string { + const B = host.B; + const wordPointer = B.tmp(), + word = B.tmp(), + ascii = B.tmp(); + B.line(`${wordPointer} = getelementptr inbounds %ScrStr, ptr ${s}, i32 0, i32 2`); + B.line(`${word} = load i32, ptr ${wordPointer}`); + B.line(`${ascii} = icmp slt i32 ${word}, 0`); + return ascii; +} + +/** The byte length of string `s`, as the target's size type. */ +export function emitStringByteLength(host: LlvmEmitterContext, s: string): string { + const B = host.B; + const lengthPointer = B.tmp(), + length = B.tmp(); + B.line(`${lengthPointer} = getelementptr inbounds %ScrStr, ptr ${s}, i32 0, i32 1`); + B.line(`${length} = load ${host.sizeType}, ptr ${lengthPointer}`); + return length; +} + +/** Unit `index` (an i64) of a proven-ASCII string `s` whose length covers + * it: one byte load, as a double. */ +export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: string): string { + const B = host.B; + const data = B.tmp(), + pointer = B.tmp(), + byte = B.tmp(), + code = B.tmp(); + B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${s}, i32 1`); + B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, i64 ${index}`); + B.line(`${byte} = load i8, ptr ${pointer}`); + B.line(`${code} = uitofp i8 ${byte} to double`); + return code; +} + /** Load the proven-ASCII flag of `%s` into `%ascii`. */ function asciiTest(): string[] { return [ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json new file mode 100644 index 000000000..feda0ecf7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7302-string-scan-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7302-string-scan-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7302-string-scan-loop-versions.ts b/tests/corpus/7302-string-scan-loop-versions.ts new file mode 100644 index 000000000..a8a975d86 --- /dev/null +++ b/tests/corpus/7302-string-scan-loop-versions.ts @@ -0,0 +1,120 @@ +// charCodeAt scans in counted for loops over ASCII strings, Latin-1 and BMP +// text, surrogate pairs and lone surrogates, empty strings, neighbors at +// i - 1 and i + 1, loops that stop early or read past the end, strings built +// by concatenation and slicing, a scan of a slice local, two strings read at +// one index, a string rebound inside or after the loop, nested scans and a +// module-level scan. + +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++; + return n; +} + +function hash(s: string): number { + let h = 0; + for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + return h; +} + +function rises(s: string): number { + let n = 0; + for (let i = 1; i < s.length; i++) if (s.charCodeAt(i) > s.charCodeAt(i - 1)) n++; + return n; +} + +function peaks(s: string): number { + let n = 0; + for (let i = 1; i < s.length - 1; i++) { + const c = s.charCodeAt(i); + if (c > s.charCodeAt(i - 1) && c > s.charCodeAt(i + 1)) n++; + } + return n; +} + +function firstDigit(s: string): number { + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c >= 48 && c <= 57) return i; + } + return -1; +} + +function pastEnd(s: string): string { + let sum = 0; + for (let i = 0; i <= s.length; i++) sum += s.charCodeAt(i); + return String(sum); +} + +function sameAt(a: string, b: string): number { + let n = 0; + const end = Math.min(a.length, b.length); + for (let i = 0; i < end; i++) if (a.charCodeAt(i) === b.charCodeAt(i)) n++; + return n; +} + +function stepped(s: string): number { + let h = 7; + for (let i = s.length - 1; i >= 0; i -= 2) h = (h * 33) ^ s.charCodeAt(i); + return h; +} + +function slicedScan(s: string, from: number, to: number): number { + const piece = s.slice(from, to); + let n = piece.length; + for (let i = 0; i < piece.length; i++) n += piece.charCodeAt(i); + return n; +} + +function rebound(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + n += s.charCodeAt(i); + if (i === 2) s = s.toUpperCase(); + } + return n; +} + +const samples = [ + "the quick brown fox jumps over the lazy dog", + "", + " ", + "abc123 def", + "café au lait", + "日本語のテキスト と 空白", + "emoji 😀 and 🎉 here", + "lone \uD800 surrogate \uDFFF end", + "zigzag azbycxdw", +]; +for (const s of samples) { + console.log( + JSON.stringify(s), + spaces(s), + hash(s), + rises(s), + peaks(s), + firstDigit(s), + pastEnd(s), + stepped(s), + rebound(s), + slicedScan(s, 2, 9), + ); +} + +let built = ""; +for (let k = 0; k < 40; k++) built += String.fromCharCode(65 + (k % 26)) + (k % 3 === 0 ? " " : ""); +const sliced = built.slice(5, 30); +console.log("built", spaces(built), hash(built), rises(sliced), peaks(sliced), sameAt(built, sliced)); +console.log("pairs", sameAt("hello world", "help, world!"), sameAt("abc", ""), sameAt("é", "e")); + +let total = 0; +for (const s of samples) { + for (let i = 0; i < s.length; i++) total = (total + s.charCodeAt(i) * (i + 1)) | 0; +} +console.log("nested", total); + +let text = "module level scan, with commas, and spaces"; +let commas = 0; +for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 44) commas++; +text = "rebound after the loop"; +console.log("module", commas, text.length, spaces(text)); diff --git a/tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json b/tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json new file mode 100644 index 000000000..793171eba --- /dev/null +++ b/tests/harness/corpus-costs/7302-string-scan-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6300 } From 057cc02326c9f276e32b4e0aadc21427a29c499f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:43:08 -0500 Subject: [PATCH 027/169] Order a spread source's keys after key-order witnesses settle The merge helper for a fixed-shape spread into an index-signature record wrote the source's keys in the shape's declared order when it was lowered. Key-order witnesses can replace that order afterwards with the order the program builds the source's values in, so a source declared `{ start, end }` but built as `{ end, start }` merged as `start,end` where Node gives `end,start`. The source's writes now go into one block that a shape-order finalizer fills in the settled order, as the other order-snapshotting helpers do. Corpus program 6916 covers sources built in another order than declared, with an absent optional field and with two spreads. --- .../lowering/containers/indexed-objects.ts | 28 ++++++++++++------- .../6916-index-record-spread-overrides.ts | 17 +++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index 75d584e4b..668603d47 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1589,16 +1589,12 @@ export function lowerIndexMergeHelper( // own planned lift (absent optionals skip — the unset convention); // no overflow exists to walk. if (plan.fields !== null) { - // Keys enter in DECLARATION order, as the object's own keys do - // (declaredOrder omits internal '%'-fields, which never copy). + // Keys enter in the source's enumeration order, as its own keys do. + // That order settles only after lowering (key-order witnesses), so + // the writes go into one block that a finalizer orders. lowerer.keyOrder.observed.add(from.id); - const orderedFields = from.declaredOrder - ? from.declaredOrder.flatMap((n) => { - const f = from.fields.find((x) => x.name === n); - return f ? [f] : []; - }) - : from.fields; - for (const ff of orderedFields) { + const fieldStmts = new Map(); + for (const ff of from.fields) { const lift = plan.fields.get(ff.name)!; const raw: IrExpr = { kind: "recordGet", @@ -1623,7 +1619,8 @@ export function lowerIndexMergeHelper( overflowOnly: true, loc, }; - body.push( + fieldStmts.set( + ff.name, utag >= 0 && ff.type.kind === "union" ? { kind: "if", @@ -1635,6 +1632,17 @@ export function lowerIndexMergeHelper( : write, ); } + // declaredOrder omits internal '%'-fields, which never copy. + const ordered = (shape: IrRecordShape): IrStmt[] => + (shape.declaredOrder ?? shape.fields.map((f) => f.name)).flatMap((field) => { + const stmt = fieldStmts.get(field); + return stmt ? [stmt] : []; + }); + const group: IrStmt & { kind: "block" } = { kind: "block", body: ordered(from), loc }; + lowerer.shapeOrderHelperFinalizers.push(() => { + group.body = ordered(lowerer.shapes.get(plan.shapeId) ?? from); + }); + body.push(group); return; } const fIv = from.indexValue!; diff --git a/tests/corpus/6916-index-record-spread-overrides.ts b/tests/corpus/6916-index-record-spread-overrides.ts index 094b17f02..0a33b0e1b 100644 --- a/tests/corpus/6916-index-record-spread-overrides.ts +++ b/tests/corpus/6916-index-record-spread-overrides.ts @@ -41,3 +41,20 @@ const labels: Record = { first: "a", ...{ zeta: "z", alpha: "a" console.log(Object.entries(labels).map(([k, v]) => k + "=" + v).join(" ")); const flags: { [key: string]: boolean } = { ...counts, mid: true, zeta: false, alpha: true }; console.log(JSON.stringify(flags)); + +// A source whose values are built in another order than its interface +// declares: the spread follows the order the values are built in. +interface Span { + start: number; + end: number; + label?: string; +} +function span(start: number, size: number, label?: string): Span { + return label === undefined ? { end: start + size, start } : { label, end: start + size, start }; +} +const spans: Record = { ...span(1, 4), step: 2 }; +console.log(Object.keys(spans).join(","), JSON.stringify(spans)); +const named: Record = { first: 0, ...span(5, 2, "w"), last: 9 }; +console.log(Object.keys(named).join(","), JSON.stringify(named)); +const both: Record = { ...counts, ...{ end: 3, start: 1 }, mid: 7 }; +console.log(Object.keys(both).join(","), JSON.stringify(both)); From a6a1eb2b95c7b2c4d1ea436526151aba44a110c1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:57:53 -0500 Subject: [PATCH 028/169] Support Duplex.from, compose stages, readable.compose and pipeline function destinations --- .../builtin-apis/streams-and-readline.mdx | 15 +- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 61 ++- .../compiler/src/backend/llvm/lib-dispatch.ts | 2 +- .../compiler/src/backend/llvm/lib-shared.ts | 6 + .../compiler/src/backend/llvm/lib-stream.ts | 26 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-calls.ts | 4 +- .../src/frontend/lowering/lower-stmts.ts | 23 +- .../src/frontend/lowering/lower-stream.ts | 444 +++++++++++++++--- packages/compiler/src/ir/builtin-effects.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 19 +- packages/compiler/src/ir/runtime-features.ts | 6 + packages/compiler/src/ir/validate.ts | 46 +- .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7252-duplex-from-generator.ts.json | 6 + .../tests/corpus/7253-duplex-from.mjs.json | 6 + .../corpus/7254-compose-functions.ts.json | 6 + .../tests/corpus/7255-compose-stages.mjs.json | 6 + .../corpus/7256-readable-compose.mjs.json | 6 + ...57-pipeline-generator-destination.mjs.json | 6 + ...258-pipeline-generator-destination.ts.json | 6 + ...9-promises-pipeline-stage-options.cjs.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_stream.c | 229 +++++++-- packages/runtime/src/scr_stream.h | 18 + packages/runtime/src/scr_stream_ops.c | 232 +++++++++ tests/corpus/7252-duplex-from-generator.ts | 69 +++ tests/corpus/7253-duplex-from.mjs | 48 ++ tests/corpus/7254-compose-functions.ts | 56 +++ tests/corpus/7255-compose-stages.mjs | 55 +++ tests/corpus/7256-readable-compose.mjs | 53 +++ .../7257-pipeline-generator-destination.mjs | 61 +++ .../7258-pipeline-generator-destination.ts | 47 ++ .../7259-promises-pipeline-stage-options.cjs | 65 +++ .../7252-duplex-from-generator.ts.json | 1 + .../corpus-costs/7253-duplex-from.mjs.json | 1 + .../7254-compose-functions.ts.json | 1 + .../corpus-costs/7255-compose-stages.mjs.json | 1 + .../7256-readable-compose.mjs.json | 1 + ...57-pipeline-generator-destination.mjs.json | 1 + ...258-pipeline-generator-destination.ts.json | 1 + ...9-promises-pipeline-stage-options.cjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 49 files changed, 1510 insertions(+), 161 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json create mode 100644 tests/corpus/7252-duplex-from-generator.ts create mode 100644 tests/corpus/7253-duplex-from.mjs create mode 100644 tests/corpus/7254-compose-functions.ts create mode 100644 tests/corpus/7255-compose-stages.mjs create mode 100644 tests/corpus/7256-readable-compose.mjs create mode 100644 tests/corpus/7257-pipeline-generator-destination.mjs create mode 100644 tests/corpus/7258-pipeline-generator-destination.ts create mode 100644 tests/corpus/7259-promises-pipeline-stage-options.cjs create mode 100644 tests/harness/corpus-costs/7252-duplex-from-generator.ts.json create mode 100644 tests/harness/corpus-costs/7253-duplex-from.mjs.json create mode 100644 tests/harness/corpus-costs/7254-compose-functions.ts.json create mode 100644 tests/harness/corpus-costs/7255-compose-stages.mjs.json create mode 100644 tests/harness/corpus-costs/7256-readable-compose.mjs.json create mode 100644 tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json create mode 100644 tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json create mode 100644 tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 2a514a5a0..060a0bf95 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -12,16 +12,21 @@ An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value. `readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`). `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error. - `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. + `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument. A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node. Stages may take the `{ signal }` argument. - An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, and function stages or iterable sources together with pipeline options are unsupported. + With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`. + `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`). + In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error. `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. - `readable.compose` is unsupported. - `stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. - `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. + `Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable. + A lone `Readable` or `Writable`, a promise, a `{ readable, writable }` pair, a web stream, and a body function that declares the options argument are unsupported in `Duplex.from`. + `stream.compose` accepts streams and async generator functions in any position, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. + Web streams as compose stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. + `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. + `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported. `setDefaultEncoding()` takes any encoding Node knows. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a7a5e718a..a1b673630 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v27"); + expect(error?.stderr).toContain("scr_runtime_abi_v28"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index abbd023d4..380f5d814 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3780,9 +3780,8 @@ declare module "util" { export type TextDecoder = globalThis.TextDecoder; export var TextEncoder: typeof globalThis.TextEncoder; export var TextDecoder: typeof globalThis.TextDecoder; - /* stream.pipeline's (...streams: unknown[]) => unknown is assignable to - * every other target's parameter type, so its overload comes first; no - * other target is assignable to it. */ + /* stream.pipeline's overload comes first: its __promisify__ member keeps + * every other target out of it. */ export function promisify( fn: typeof import("node:stream").pipeline, ): typeof import("node:stream/promises").pipeline; @@ -7266,6 +7265,8 @@ declare module "stream" { _destroy(error: Error | null, callback: (error?: Error | null) => void): void; [Symbol.asyncIterator](): AsyncIterableIterator; iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; + /* compose(this, stage), destroyed when options.signal aborts. */ + compose(stage: ComposeStage, options?: { signal: AbortSignal }): Duplex; push(chunk: Buffer | string | null, encoding?: string): boolean; /* An object-mode stream's values. */ push(chunk: any, encoding?: string): boolean; @@ -7456,6 +7457,9 @@ declare module "stream" { * mirrors @types/node: extends Readable, the writable half declared * again. The lowering treats both halves as first-class. */ class Duplex extends Readable { + /* Duplex.from: an async generator function, an iterable, a string or + * a stream (duplexify.js). */ + static from(body: unknown): Duplex; constructor(opts: ObjectDuplexOptions); constructor(opts: StringDuplexOptions); constructor(opts?: DuplexOptions); @@ -7576,11 +7580,36 @@ declare module "stream" { /* addAbortSignal(signal, stream): the stream, destroyed with an * AbortError (cause: signal.reason) when the signal aborts. */ function addAbortSignal(signal: AbortSignal, stream: T): T; - /* Declared so requires typecheck; each fences at its use site. */ - function pipeline(...streams: unknown[]): unknown; - /* compose(...streams): a Duplex that writes to the first stream and - * reads from the last, with the streams running as a pipeline. */ - function compose(...streams: Array): Duplex; + /* pipeline(...stages, callback): the last stream, or the object-mode + * PassThrough that a function destination feeds. */ + type PipelineResult = T extends [ + ...unknown[], + infer D, + (...args: any[]) => unknown, + ] + ? D extends (...args: any[]) => unknown + ? PassThrough + : D + : unknown; + function pipeline(...streams: T): PipelineResult; + /* util.promisify's pipeline overload matches only pipeline itself: a + * generic signature alone admits every function. */ + namespace pipeline { + const __promisify__: typeof promises.pipeline; + } + /* A compose stage: a stream, an async generator function over the + * previous stage, or (first) an iterable, async iterable or web + * ReadableStream. */ + type ComposeStage = + | Readable + | Writable + | Iterable + | AsyncIterable + | globalThis.ReadableStream + | ((source: any, options?: { signal: AbortSignal }) => unknown); + /* compose(...streams): a Duplex that writes to the first stage and + * reads from the last, with the stages running as a pipeline. */ + function compose(...streams: ComposeStage[]): Duplex; /* finished's options: readable/writable choose which ends count, * error: false ignores 'error' events, signal aborts the wait. */ interface FinishedOptions { @@ -7599,13 +7628,20 @@ declare module "stream" { * else 65536 — Node's own state.js split); literal argument only. */ function getDefaultHighWaterMark(objectMode: boolean): number; /* The promise pipeline's result: a last stage that is an async - * function resolves the promise with its value. */ + * function resolves the promise with its value (before the options + * argument, when one follows). */ type PipelinePromise = T extends [ ...unknown[], (...args: any[]) => Promise, ] ? Promise

- : Promise; + : T extends [ + ...unknown[], + (...args: any[]) => Promise, + { signal?: AbortSignal; end?: boolean }, + ] + ? Promise

+ : Promise; namespace promises { function pipeline(...streams: T): PipelinePromise; function finished( @@ -7635,6 +7671,7 @@ declare module "stream" { DuplexOptions, TransformOptions, FinishedOptions, + PipelinePromise, }; } declare module "node:stream" { @@ -7649,9 +7686,7 @@ declare module "stream/promises" { * pipeline's trailing { signal, end } object and finished's options * (readable, writable, error, cleanup, signal) are supported. A last * stage that is an async function resolves the promise with its value. */ - function pipeline( - ...streams: T - ): T extends [...unknown[], (...args: any[]) => Promise] ? Promise

: Promise; + function pipeline(...streams: T): import("stream").PipelinePromise; function finished( stream: unknown, options?: { diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 24f2bf019..fd5b5d681 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -734,7 +734,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e); if ( e.fn === "sp.pipeline" || - e.fn === "sp.pipelineStages" || + e.fn.startsWith("sp.pipelineStages") || e.fn.startsWith("sp.pipelineOpts") || e.fn.startsWith("sp.finishedOpts") ) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 46bfc279e..94318c3aa 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1127,6 +1127,8 @@ export const LIB_FN_SYMS: Record = { "readable.pushEncoding": "scr_stream_set_push_encoding", "stream.objectMode": "scr_stream_object_mode", "stream.decodeStrings": "scr_stream_decode_strings", + "stream.duplexFromGen": "scr_stream_duplex_from_gen", + "stream.duplexFromDyn": "scr_stream_duplex_from_dyn", "writable.writeEncoding": "scr_stream_write_encoding", "writable.setDefaultEncoding": "scr_stream_set_default_encoding", "stream.chunkDone": "scr_stream_chunk_done", @@ -1733,6 +1735,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setTransform", "stream.setFlush", "stream.setConstruct", + "stream.duplexFromGen", + "stream.duplexFromDyn", "stream.setWritev", "stream.setWritevDefault", "process.activeResources", @@ -1744,8 +1748,10 @@ export const USES_TIMERS_LIB_FNS = new Set([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.pipelineStages", "stream.pipelineStagesDyn", + "stream.composeStages", "sp.pipelineOpts", "sp.pipelineOptsSignal", "sp.finishedOpts", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 4611b3bb8..5fd4af1a9 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -701,12 +701,15 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV } if ( e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || e.fn === "stream.pipelineStages" || - e.fn === "stream.pipelineStagesDyn" + e.fn === "stream.pipelineStagesDyn" || + e.fn === "stream.composeStages" ) { // (n, k, positions, flags, mask, s1..sn, stage1..stagek, helpers..., - // box?, (signal, abort)?[, cb]): the streams, the stage closures and - // the helpers ride stack arrays; absent groups pass null. + // box?, (signal, abort)?[, cb | end, signal?]): the streams, the + // stage closures and the helpers ride stack arrays; absent groups + // pass null. const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args; if ( nArg?.kind !== "numLit" || @@ -719,7 +722,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const n = nArg.value; const k = kArg.value; const flags = flagsArg.value; - const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value); + const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value, flags); const args = e.args.map((a) => host.emitExpr(a)); const packed = (from: number, count: number): string => { if (count === 0) return "null"; @@ -749,6 +752,21 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV if (e.fn === "sp.pipelineStages") { host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`); B.line(`${t} = call ptr @scr_sp_pipeline_stages(${head})`); + } else if (e.fn === "sp.pipelineStagesOpts") { + const end = args[at++]!.name; + const outer = take((flags & 64) !== 0); + let add = "null"; + if (outer !== "null") { + host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`); + add = "@scr_events_add_abort_listener"; + } + host.declare(`declare ptr @scr_sp_pipeline_stages_opts(${params}, i1, ptr, ptr)`); + B.line( + `${t} = call ptr @scr_sp_pipeline_stages_opts(${head}, i1 ${end}, ptr ${outer}, ptr ${add})`, + ); + } else if (e.fn === "stream.composeStages") { + host.declare(`declare ptr @scr_stream_compose_stages(${params})`); + B.line(`${t} = call ptr @scr_stream_compose_stages(${head})`); } else if (e.fn === "stream.pipelineStagesDyn") { host.declare(`declare ptr @scr_stream_pipeline_stages_dyn(${params}, ptr)`); B.line( diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 3a41d9176..a616dbec2 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 27 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; +export const RUNTIME_ABI_VERSION = 28 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index af61e5229..c2536ede8 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index d09509911..33b1ca38e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..875fb9e7a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -9843,7 +9843,9 @@ export function lowerObjectMethodCall( if ( info && !found && - (STREAM_API_MEMBERS.has(access.name.text) || STREAM_OPERATORS.has(access.name.text)) && + (STREAM_API_MEMBERS.has(access.name.text) || + STREAM_OPERATORS.has(access.name.text) || + access.name.text === "compose") && streamSidesOf(lowerer, info) !== null ) { const stream = lowerStreamMethodCall(lowerer, call, access, info); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..305cbd9b4 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2,7 +2,7 @@ import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; -import { lowerForAwaitDynStream } from "./dyn-streams.js"; +import { lowerForAwaitDynStream, nodeStreamInterfaceSide } from "./dyn-streams.js"; import { lowerForAwaitValueStream } from "./builtins/hash-stream.js"; import { lowerChildPropertyWrite } from "./handle-properties.js"; import { @@ -163,7 +163,11 @@ import { objectFactorySignature } from "./object-factory-new.js"; import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js"; import { classForInHasKey, classPropertiesHelper } from "./class-dynamic-dispatch.js"; import { genericIfaceBindingKeepsClass, superConditionLead } from "./lower-classes.js"; -import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js"; +import { + lowerStreamUnderscoreAssign, + streamClassAliasDecl, + streamSidesOf, +} from "./lower-stream.js"; import { staticAssertInstanceDecl } from "./assert-instances.js"; import { lowerHttpResPropertyAssignment, @@ -9451,6 +9455,21 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ) { return lowerForAwaitStdin(lowerer, stmt); } + // NodeJS.WritableStream is what @types/node answers for pipeline() + // with a function destination, whose value is a PassThrough. + if (nodeStreamInterfaceSide(lowerer, lowerer.typeOf(stmt.expression)) === "w") { + const mapped = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression)); + const sides = + mapped?.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(mapped.className)) + : null; + if (sides !== "r" && sides !== "rw") + lowerer.noLowering( + "for await over a NodeJS.WritableStream", + stmt.expression, + "type the stream as the readable class it is: pipeline() answers a PassThrough for a function destination (pipeline(...) as PassThrough, or a JSDoc @type)", + ); + } // A checked-dynamic NodeJS.ReadableStream iterates its native stream, // and a Hash, Hmac, Cipheriv or Decipheriv iterates its Transform. const dynStream = diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 20f156124..214a2402e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -56,6 +56,7 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js"; import { LiftedFrame, driveGenerator } from "./containers/iterator-helpers.js"; import { lowerArrayIteratorValue, lowerStringIteratorValue } from "./iterator-adapters.js"; +import { staticReadableStreamElementType } from "./lower-stdlib.js"; import { BOOL, DYN, @@ -1816,8 +1817,23 @@ export function lowerStreamStaticCall( if (member !== "from") { lowerer.noLowering(`${cls.slice(1)}.${member}`, call); } + if (cls === "%Duplex") { + const fromArgs = call.arguments.nodes; + if (fromArgs.length !== 1) { + lowerer.noLowering( + `Duplex.from with ${fromArgs.length} arguments`, + call, + "Duplex.from takes one body", + ); + } + return duplexFromBody(lowerer, fromArgs[0]!, locOf(call)); + } if (cls !== "%Readable") { - lowerer.noLowering(`${cls.slice(1)}.from`, call, "Readable.from is the lowered form"); + lowerer.noLowering( + `${cls.slice(1)}.from`, + call, + "Readable.from and Duplex.from are the lowered forms", + ); } const args = call.arguments.nodes; if (args.length > 2) { @@ -1827,16 +1843,34 @@ export function lowerStreamStaticCall( "Readable.from(iterable, options) is the supported form", ); } - const type: IrType = { kind: "object", className: "%Readable" }; + return lowerFromSource(lowerer, call, args[0], args[1], false); +} + +/** Readable.from(source, options), and for Duplex.from over an iterable + * (duplex) duplexify.js's from(Duplexify, body, { objectMode: true, + * writable: false }): the same builders with SCR_FROM_DUPLEX, answering a + * Duplexify without a writable side. */ +function lowerFromSource( + lowerer: Lowerer, + at: ts.Node, + srcNode: ts.Expression | undefined, + optsNode: ts.Expression | undefined, + duplex: boolean, +): IrExpr { + const loc = locOf(at); + const call = at; + const type: IrType = { kind: "object", className: duplex ? "%Duplex" : "%Readable" }; + const what = duplex ? "Duplex.from" : "Readable.from"; + // SCR_FROM_DUPLEX + const duplexFlag = duplex ? 32 : 0; const js = isJsSourceFile(ts.getSourceFileOfNode(call)); - const srcNode = args[0]; - const optsNode = args[1]; const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null; const typedElem = (t: IrType | null | undefined): "string" | "bytes" | null => t?.kind === "string" ? "string" : t?.kind === "bytes" && t.elem === "u8" ? "bytes" : null; const single = typedElem(srcT); const elem = srcT?.kind === "array" ? typedElem(srcT.elem) : null; - const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + const opts0 = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + const opts = opts0 ? { ...opts0, flags: opts0.flags | duplexFlag } : opts0; // A class whose [Symbol.iterator]() or [Symbol.asyncIterator]() is a // generator method: from.js calls the method when the stream is built, // then reads the generator like any other. @@ -1934,6 +1968,7 @@ export function lowerStreamStaticCall( // The checked-dynamic form: the runtime takes from.js's ladder // (ERR_INVALID_ARG_TYPE for a non-iterable, the options, null values). const src = srcNode ? lowerer.lowerExprExpecting(srcNode, DYN) : dynUndefinedExpr(loc); + if (duplex) return { kind: "libCall", fn: "stream.duplexFromDyn", args: [src], type, loc }; const o = lowerOperatorValue(lowerer, optsNode, "Readable.from options", loc, true); return { kind: "libCall", fn: "readable.fromDyn", args: [src, o], type, loc }; } @@ -1945,7 +1980,7 @@ export function lowerStreamStaticCall( ); } lowerer.noLowering( - `Readable.from over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, + `${what} over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, srcNode ?? call, "the supported sources are a string or Buffer, and arrays, Sets, Maps, generators and class iterables of values that convert to checked-dynamic values", ); @@ -2762,22 +2797,28 @@ export function lowerStreamModuleCall( : null; const stageArgs = opts ? args.slice(0, -1) : args; if (hasPipelineFunctionStage(lowerer, stageArgs)) { - if (opts) { - lowerer.noLowering( - "pipeline with function stages or iterable sources and options", - lastArg, - "pipeline(source, async function* (source) { ... }, destination) without options is supported", - ); - } const staged = lowerPipelineStages(lowerer, stageArgs, loc); + const type = staged.destPromise ?? { kind: "promise", inner: VOID }; // A promise destination's value settles the result (same type). - return staged.wrap({ - kind: "libCall", - fn: "sp.pipelineStages", - args: staged.args, - type: staged.destPromise ?? { kind: "promise", inner: VOID }, - loc, - }); + if (!opts) { + return staged.wrap({ + kind: "libCall", + fn: "sp.pipelineStages", + args: staged.args, + type, + loc, + }); + } + // the options: end, and the signal (flags bit 6) + const flagsArg = staged.args[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + const args = [...staged.args, boolLit(opts.end, loc)]; + if (opts.signal) { + args[3] = numLit(flagsArg.value | 64, loc); + args.push(opts.signal); + } + return staged.wrap({ kind: "libCall", fn: "sp.pipelineStagesOpts", args, type, loc }); } let streams: IrExpr[]; try { @@ -2958,22 +2999,27 @@ export function lowerStreamModuleCall( } if (hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc); - if (staged.dest !== 0) { - // callback(err, value): the destination's value reaches the - // callback through the checked-dynamic path. Node answers an - // object-mode PassThrough here; reading it has no lowering. - if (!ts.isExpressionStatement(call.parent)) { - lowerer.noLowering( - "using the PassThrough that pipeline() answers for a function destination", - call, - "read the destination's value in the callback", - ); - } + const cbNode = args[args.length - 1]!; + const cbParams = + ts.isArrowFunction(cbNode) || ts.isFunctionExpression(cbNode) + ? cbNode.parameters.nodes.length + : 0; + if (staged.dest === 2 || (staged.dest === 1 && cbParams > 1)) { + // callback(err, value): the destination's value (undefined for + // an async generator) reaches the callback through the + // checked-dynamic path. const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); + // SCR_PIPE_TYPED_CB: typed code's callback takes null for no error + const stagedArgs = [...staged.args]; + const flagsArg = stagedArgs[3]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("pipeline flags not a literal"); + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) + stagedArgs[3] = numLit(flagsArg.value | 128, loc); return staged.wrap({ kind: "libCall", fn: "stream.pipelineStagesDyn", - args: [...staged.args, cb], + args: [...stagedArgs, cb], type: staged.last, loc, }); @@ -3010,7 +3056,12 @@ export function lowerStreamModuleCall( }; } - if (bi.member === "compose") return lowerStreamCompose(lowerer, call, loc); + if (bi.member === "compose") { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); + } + return lowerStreamCompose(lowerer, call.arguments.nodes, loc); + } return null; } @@ -3227,7 +3278,7 @@ function lowerIterableSource( interface PipelineStagesLowering { args: IrExpr[]; last: IrType; - dest: 0 | 2; + dest: 0 | 1 | 2; destPromise: (IrType & { kind: "promise" }) | null; wrap: (call: IrExpr) => IrExpr; } @@ -3235,9 +3286,10 @@ interface PipelineStagesLowering { /** pipeline(...) with function stages (pipeline.js). A function may * start the pipeline (a source function, called with `{ signal }`), may * end it (an async function whose promise's value reaches the callback or - * the promise), and runs of async generator functions may sit between - * streams; the last generator of a run that a stream follows is pumped - * into that stream. Each stage's `source` parameter is typed as what the + * the promise, or an async generator function), and runs of async + * generator functions may sit between streams; the last generator of a + * run that a stream follows is pumped into that stream. A function + * destination feeds the object-mode PassThrough that pipeline() answers. Each stage's `source` parameter is typed as what the * pipeline passes it: makeAsyncIterable answers the previous stream * itself (a Readable is async iterable) or the previous stage's * generator. */ @@ -3245,6 +3297,7 @@ function lowerPipelineStages( lowerer: Lowerer, stageArgs: readonly ts.Expression[], loc: SrcLoc, + preLowered?: ReadonlyMap, ): PipelineStagesLowering { if (stageArgs.length > 52) { lowerer.noLowering( @@ -3258,7 +3311,7 @@ function lowerPipelineStages( const helperArgs: IrExpr[] = []; let positions = 0; let mask = 0; - let dest: 0 | 2 = 0; + let dest: 0 | 1 | 2 = 0; let destPromise: (IrType & { kind: "promise" }) | null = null; // What the next function stage receives: null before the first stage, // else the previous stream's type or the previous stage's generator. @@ -3269,14 +3322,19 @@ function lowerPipelineStages( const argOrder: { list: IrExpr[]; index: number }[] = []; for (let j = 0; j < stageArgs.length; j++) { const arg = stageArgs[j]!; - const fn = pipelineStageFunction(lowerer, arg); + // compose's head and tail functions arrive as Duplex.from streams + const fn = preLowered?.has(j) ? null : pipelineStageFunction(lowerer, arg); const syncSourceFn = j === 0 && fn !== null && !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined && !(fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); - if (j === 0 && (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null))) { + if ( + j === 0 && + !preLowered?.has(j) && + (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null)) + ) { const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); if (source === null) lowerer.noLowering("pipeline over this source", arg); positions += 1; @@ -3302,7 +3360,7 @@ function lowerPipelineStages( } let stream: IrExpr; try { - stream = lowerStreamArg(lowerer, arg, "pipeline"); + stream = preLowered?.get(j) ?? lowerStreamArg(lowerer, arg, "pipeline"); } catch (e) { if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); throw e; @@ -3315,15 +3373,6 @@ function lowerPipelineStages( const isLast = j === stageArgs.length - 1; const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; - if (isLast && isGen) { - // pipeline() pumps it into an object-mode PassThrough, whose - // buffering decides when the pipeline finishes - lowerer.noLowering( - "a pipeline destination that is an async generator function", - arg, - "end the pipeline with a stream or an async function (source) { for await (const chunk of source) ... }", - ); - } if (!isAsync || (!isGen && !isLast)) { lowerer.noLowering( isLast @@ -3362,6 +3411,14 @@ function lowerPipelineStages( dest = 2; destPromise = lowered.out; srcT = null; + } else if (isLast) { + // an async generator destination: pumped into the object-mode + // PassThrough that pipeline() answers + const helpers = fromGenHelpers(lowerer, lowered.out, loc); + if (!helpers) lowerer.noLowering("a pipeline destination with this generator type", arg); + helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); + dest = 1; + srcT = null; } else { srcT = lowered.out; } @@ -3397,7 +3454,8 @@ function lowerPipelineStages( argDecls.length === 0 ? call : { kind: "seqExpr", stmts: argDecls, result: call, type: call.type, loc }; - // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and abort. + // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and + // abort (bit 6, the promise form's options signal, is set by its caller). const flags = (dest << 1) | (mask !== 0 ? 32 : 0); const head: IrExpr[] = [ numLit(streams.length, loc), @@ -3410,7 +3468,7 @@ function lowerPipelineStages( ...helperArgs, ]; const last: IrType = - dest === 2 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; + dest !== 0 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; if (mask === 0) return { args: head, last, dest, destPromise, wrap: bindArgs }; // const ac = new AbortController(): its signal goes to the stages, and // finishImpl's ac.abort() runs through the abort closure. @@ -3622,6 +3680,166 @@ function lowerStageFunction( } } +/** The async generator a Duplex.from body iterates (duplexify.js's + * fromAsyncGen source), as a closure the runtime calls with next() and + * ack(): each pass awaits next() (the deferred a write or the final + * settles), calls ack() (process.nextTick(cb) and a fresh deferred), + * returns at the done marker, and yields the chunk. */ +function duplexSourceHelper(lowerer: Lowerer, genT: SyncGenType, loc: SrcLoc): IrExpr | null { + const nextT = funcOf([], { kind: "promise", inner: DYN }); + const ackT = funcOf([], VOID); + const type = funcOf([nextT, ackT], genT); + const key = "duplexSource"; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const resultType = genResultRecord(DYN, VOID, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + name = `%stream.duplexSource.${lowerer.valueHelpers.size}`; + const frame = new LiftedFrame(loc); + const nextId = frame.param("next", nextT); + const ackId = frame.param("ack", ackT); + const valueId = frame.local("value", DYN, false); + const value = varRef(valueId, DYN, loc); + const body: IrStmt[] = [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "varDecl", + localId: valueId, + init: { + kind: "awaitExpr", + value: { + kind: "callValue", + callee: varRef(nextId, nextT, loc), + args: [], + type: { kind: "promise", inner: DYN }, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef(ackId, ackT, loc), + args: [], + type: VOID, + loc, + }, + loc, + }, + { + kind: "if", + cond: { kind: "libCall", fn: "stream.chunkDone", args: [value], type: BOOL, loc }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value, type: VOID, loc }, loc }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: frame.params, + returnType: VOID, + async: true, + locals: frame.locals, + body, + generator: { yieldT: DYN, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type, loc }; +} + +/** Duplex.from(body) (duplexify.js) for the bodies with a lowering: an + * async generator function, whose source generator the Duplex's writes + * feed (stream.duplexFromGen); a duplex stream, which answers itself; and + * the bodies Readable.from reads (iterables, strings, generators), as a + * Duplexify that is not writable. */ +function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr { + const duplex: IrType = { kind: "object", className: "%Duplex" }; + const fn = pipelineStageFunction(lowerer, node); + if (fn !== null) { + const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; + if (!isAsync || !isGen) { + lowerer.noLowering( + "Duplex.from over a function that is not an async generator function", + node, + "write the body as async function* (source) { for await (const chunk of source) ... }", + ); + } + if (fn.parameters.nodes.length > 1) { + lowerer.noLowering( + "a Duplex.from body that declares the options argument", + fn.parameters.nodes[1]!, + "the body takes only its source", + ); + } + const sourceT: SyncGenType = { + kind: "generator", + async: true, + yieldT: DYN, + retT: VOID, + nextT: UNDEFINED_T, + }; + const lowered = lowerStageFunction(lowerer, fn, sourceT); + const helpers = + lowered.out.kind === "generator" ? fromGenHelpers(lowerer, lowered.out, loc) : null; + const source = duplexSourceHelper(lowerer, sourceT, loc); + if (!helpers || !source) + lowerer.noLowering("Duplex.from over a function with this generator type", node); + return { + kind: "libCall", + fn: "stream.duplexFromGen", + args: [lowered.closure, source!, ...helpers!], + type: duplex, + loc, + }; + } + // duplexify.js: a stream answers itself (a Duplex) or is wrapped; any + // other body reads as from(Duplexify, body, { objectMode: true, + // writable: false }) + let bodyT: IrType | null; + try { + bodyT = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + bodyT = null; + } + const bodySides = + bodyT?.kind === "object" ? streamSidesOf(lowerer, lowerer.classes.get(bodyT.className)) : null; + if (bodySides === null) return lowerFromSource(lowerer, node, node, undefined, true); + let stream: IrExpr; + try { + stream = lowerStreamArg(lowerer, node, "compose"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) return e.expr; + throw e; + } + const sides = + stream.type.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(stream.type.className)) + : null; + if (sides !== "rw") { + lowerer.noLowering( + `Duplex.from over a ${sides === "r" ? "Readable" : sides === "w" ? "Writable" : "value of this type"}`, + node, + "an async generator function, an array, a string, a generator, an async generator, or a Duplex, Transform or PassThrough is the lowered body", + ); + } + return stream; +} + /** The checker type of a stage's string or Buffer chunks, or null for * other chunk types. */ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts.Type | null { @@ -3636,12 +3854,8 @@ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts * classes fix which positions can be readable and writable; a stream in * the wrong position, a single Readable or Writable (Duplex.from wraps * those), and functions, iterables or web streams have no lowering. */ -function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = call.arguments.nodes; +function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], loc: SrcLoc): IrExpr { const duplex: IrType = { kind: "object", className: "%Duplex" }; - if (args.some((a) => ts.isSpreadElement(a))) { - lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); - } if (args.length === 0) { return nodeThrowExpr( 1, @@ -3651,9 +3865,43 @@ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL loc, ); } + for (const a of args) { + // (typeOf on a stage function's name would report what collecting its + // declared signature deferred) + if (pipelineStageFunction(lowerer, a) !== null) continue; + if (staticReadableStreamElementType(lowerer, a) !== null) { + lowerer.noLowering( + "compose with a web ReadableStream", + a, + "web streams do not convert to Node streams yet; compose Node streams, iterables or async generator functions", + ); + } + } + // Duplex.from(stage) for one stage (duplexify.js), and for a function at + // either end; functions between, an iterable first stage or a source + // function run as the pipeline's function stages (compose.js) + if (args.length === 1) return duplexFromBody(lowerer, args[0]!, loc); + const last = args.length - 1; + const isFn = (i: number): boolean => pipelineStageFunction(lowerer, args[i]!) !== null; + const pre = new Map(); + if (isFn(0)) pre.set(0, duplexFromBody(lowerer, args[0]!, loc)); + if (isFn(last)) pre.set(last, duplexFromBody(lowerer, args[last]!, loc)); + const staged = + args.some((_, i) => !pre.has(i) && isFn(i)) || + (!pre.has(0) && iterableSourceType(lowerer, args[0]!) !== null); + if (staged) { + const lowered = lowerPipelineStages(lowerer, args, loc, pre); + return lowered.wrap({ + kind: "libCall", + fn: "stream.composeStages", + args: lowered.args, + type: duplex, + loc, + }); + } let streams: IrExpr[]; try { - streams = args.map((a) => lowerStreamArg(lowerer, a, "compose")); + streams = args.map((a, i) => pre.get(i) ?? lowerStreamArg(lowerer, a, "compose")); } catch (e) { if (e instanceof StreamArgTypeThrow) return e.expr; throw e; @@ -4179,6 +4427,80 @@ function pipeIntoDyn( }; } +/** readable.compose(stage, options): compose(this, stage), with + * addAbortSignal(options.signal, composed) when the signal is set + * (operators.js). */ +function lowerReadableCompose( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const loc = locOf(call); + const args = call.arguments.nodes; + if (args.length === 0 || args.length > 2 || args.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering( + `readable.compose with ${args.length === 0 ? "no stage" : "these arguments"}`, + call, + "the supported form is readable.compose(stage) or readable.compose(stage, { signal })", + ); + } + let signalNode: ts.Expression | null = null; + const options = args[1]; + if ( + options !== undefined && + options.kind !== ts.SyntaxKind.NullKeyword && + !(ts.isIdentifier(options) && options.text === "undefined") + ) { + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "readable.compose options that are not an object literal", + options, + "pass { signal } as an object literal", + ); + } + for (const p of options.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key !== "signal") { + lowerer.noLowering( + key === null + ? "readable.compose options with this member" + : `the readable.compose option '${key}'`, + p, + "signal is the readable.compose option", + ); + } + const value = ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + if (value.kind === ts.SyntaxKind.NullKeyword) continue; + if (ts.isIdentifier(value) && value.text === "undefined") continue; + // The signal is read after compose runs, so only values whose + // evaluation has no effects. + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + "the readable.compose option 'signal' with this value", + p, + "pass a variable or property read (const signal = controller.signal)", + ); + } + signalNode = value; + } + } + const composed = lowerStreamCompose(lowerer, [access.expression, args[0]!], loc); + if (signalNode === null) return composed; + return { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [composed, lowerer.lowerExprExpecting(signalNode, DYN)], + type: composed.type, + loc, + }; +} + export function lowerStreamMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -4191,6 +4513,12 @@ export function lowerStreamMethodCall( if (opSides === "r" || opSides === "rw") return lowerStreamOperator(lowerer, call, access, member); } + if (member === "compose") { + const composeSides = streamSidesOf(lowerer, info); + return composeSides === "r" || composeSides === "rw" + ? lowerReadableCompose(lowerer, call, access) + : null; + } if (!STREAM_API_MEMBERS.has(member)) return null; const sides = streamSidesOf(lowerer, info); if (!sides) return null; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 6ddee1597..bab07c458 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -340,7 +340,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.pipelineStages", + "stream.composeStages", "stream.pipelineStagesDyn", "sc.text", "sc.json", @@ -380,6 +382,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "writable.end", "writable.uncork", "writable.setDefaultEncoding", + "stream.duplexFromGen", + "stream.duplexFromDyn", "stream.destroy", "stream.destroyErr", "stream.iteratorClose", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e561c0dbf..1708ad01a 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3959,6 +3959,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.pipeline": { argTypes: [F64], result: VOID }, "stream.pipelineDyn": { argTypes: [F64], result: VOID }, "stream.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, + "stream.composeStages": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, "stream.pipelineStagesDyn": { argTypes: [F64, F64, F64, F64, F64], result: VOID }, /** stream.compose(count, s1..sn): the Duplex over a pipeline of the * streams (+1). MAY THROW (ERR_INVALID_ARG_VALUE for a stream that is @@ -4003,6 +4004,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [F64, F64, F64, F64, F64], result: { kind: "promise", inner: VOID }, }, + /** sp.pipelineStages with the options argument: the same head, then + * end (a bool) and, with flags bit 6, the options' signal (dyn). */ + "sp.pipelineStagesOpts": { + argTypes: [F64, F64, F64, F64, F64], + result: { kind: "promise", inner: VOID }, + }, /** node:stream/consumers — the promise consumers over the readable * machinery: (s) → a pending promise settled at the terminal point * with the accumulated result (sc.text: the utf8 decode, sc.json: the @@ -4060,6 +4067,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * after construction; answers the receiver (+1, the chaining shape). */ "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, + "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, /** Whether a readable.nextChunkDyn step answered the end (the done @@ -4815,10 +4824,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ export type IrLibFn = keyof typeof LIB_FN_SIGS; /** The function-stage runs of a pipeline of `total` stages that a stream - * follows (positions bit j: stage j is a function): each pumps its last - * generator into that stream through one (next, read, return) triple. */ -export function pipelinePumpedRuns(total: number, positions: number): number { - let runs = 0; + * follows (positions bit j: stage j is a function), and an async + * generator destination's run (flags bits 1-2 = 1, pumped into the + * PassThrough pipeline() answers): each pumps its last generator through + * one (next, read, return) triple. */ +export function pipelinePumpedRuns(total: number, positions: number, flags: number): number { + let runs = ((flags >> 1) & 3) === 1 ? 1 : 0; for (let j = 0; j + 1 < total; j++) { if (Math.floor(positions / 2 ** j) % 2 === 1 && Math.floor(positions / 2 ** (j + 1)) % 2 === 0) runs++; diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 555f5d306..da9cab1c3 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -219,6 +219,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.chunkDone", "stream.objectMode", "stream.decodeStrings", + "stream.duplexFromGen", + "stream.duplexFromDyn", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", @@ -231,6 +233,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "sp.finished", "sp.pipeline", "sp.pipelineStages", + "sp.pipelineStagesOpts", "stream.addAbortSignal", "stream.addAbortSignalFn", "stream.destroy", @@ -258,6 +261,7 @@ const STREAM_LIB_PREFIXES: readonly string[] = [ "sp.pipelineOpts", "stream.finishedOpts", "stream.pipelineStages", + "stream.composeStages", "stream.set", "transform.", "writable.", @@ -271,6 +275,8 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "readable.fromDone", "readable.fromDyn", "readable.fromIter", + "stream.duplexFromGen", + "stream.duplexFromDyn", ]); /** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 68bc61b21..a76784083 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4101,8 +4101,10 @@ function validateFunction( e.fn === "stream.pipelineDyn" || e.fn === "stream.compose" || e.fn === "stream.pipelineStages" || + e.fn === "stream.composeStages" || e.fn === "stream.pipelineStagesDyn" || e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || e.fn === "sp.pipeline" || e.fn === "sp.pipelineOpts" || e.fn === "sp.pipelineOptsSignal" || @@ -5495,6 +5497,8 @@ function validateFunction( e.fn === "stream.destroyErr" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.duplexFromGen" || + e.fn === "stream.duplexFromDyn" || e.fn === "stream.setConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" || @@ -5507,8 +5511,10 @@ function validateFunction( e.fn === "stream.pipelineDyn" || e.fn === "stream.compose" || e.fn === "stream.pipelineStages" || + e.fn === "stream.composeStages" || e.fn === "stream.pipelineStagesDyn" || e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || e.fn === "stream.addAbortSignal" || e.fn === "stream.addAbortSignalFn" || e.fn === "stream.finishedOpts" || @@ -5548,6 +5554,13 @@ function validateFunction( } return true; } + if (e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn") { + // Duplex.from: (stage, source, helpers...) or (body) → a Duplex. + if (!isStreamObject(e.type)) { + err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc); + } + return true; + } if (e.fn.endsWith(".newDyn")) { // The dyn-options constructor: (optsDyn) → the stream class. if (!isStreamObject(e.type)) { @@ -5618,13 +5631,15 @@ function validateFunction( if ( e.fn === "stream.pipelineStages" || e.fn === "stream.pipelineStagesDyn" || - e.fn === "sp.pipelineStages" + e.fn === "sp.pipelineStages" || + e.fn === "sp.pipelineStagesOpts" || + e.fn === "stream.composeStages" ) { // (n, k, positions, flags, mask, s1..sn, stage1..stagek, - // helpers..., box?, (signal, abort)?[, cb]): positions bit j marks - // stage j a function, flags bits 1-2 the destination, bit 5 the - // signal group; helpers are three per function run a stream - // follows. + // helpers..., box?, (signal, abort)?[, cb | end, signal?]): + // positions bit j marks stage j a function, flags bits 1-2 the + // destination, bit 5 the signal group, bit 6 the options' signal; + // helpers are three per pumped function run. const [n, k, positions, flags] = e.args .slice(0, 4) .map((a) => (a?.kind === "numLit" ? a.value : -1)); @@ -5635,9 +5650,14 @@ function validateFunction( if (Math.floor(positions! / 2 ** j) % 2 === 1) functions++; } const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1; - const helperCount = 3 * pipelinePumpedRuns(total, positions!); + const helperCount = 3 * pipelinePumpedRuns(total, positions!, flags!); const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0); - const cbArgs = e.fn === "sp.pipelineStages" ? 0 : 1; + const cbArgs = + e.fn === "sp.pipelineStagesOpts" + ? 1 + (flags! & 64 ? 1 : 0) + : e.fn === "sp.pipelineStages" || e.fn === "stream.composeStages" + ? 0 + : 1; if ( flags! < 0 || n! < 0 || @@ -5646,7 +5666,8 @@ function validateFunction( total > 52 || positions! >= 2 ** total || functions !== k || - (destKind === 2) !== lastIsFunction || + destKind === 3 || + (destKind !== 0) !== lastIsFunction || total < 2 || e.args.length !== 5 + n! + k! + groups + cbArgs ) { @@ -5674,12 +5695,19 @@ function validateFunction( if (e.args[at2]?.type.kind !== "dyn" || e.args[at2 + 1]?.type.kind !== "func") err(`libCall ${e.fn} signal and abort must be a dyn and a func`, e.loc); } - if (cbArgs) { + if (e.fn === "sp.pipelineStagesOpts") { + const tail = e.args.slice(e.args.length - cbArgs); + if (tail[0]?.type.kind !== "bool" || (cbArgs > 1 && tail[1]?.type.kind !== "dyn")) + err(`libCall ${e.fn} end and signal must be a bool and a dyn`, e.loc); + if (e.type.kind !== "promise") err(`libCall ${e.fn} must return a promise`, e.loc); + } else if (cbArgs) { const cbK = e.args[e.args.length - 1]?.type.kind; if (e.fn === "stream.pipelineStages" ? cbK !== "func" : cbK !== "dyn") { err(`libCall ${e.fn} callback has the wrong kind`, e.loc); } if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc); + } else if (e.fn === "stream.composeStages") { + if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc); } else if (e.type.kind !== "promise") { err(`libCall ${e.fn} must return a promise`, e.loc); } diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 35a747d60..71b9570b4 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v28"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json new file mode 100644 index 000000000..d29eed123 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7252-duplex-from-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7252-duplex-from-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json new file mode 100644 index 000000000..e12ce5a52 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7253-duplex-from.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7253-duplex-from.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json new file mode 100644 index 000000000..07e468aa2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7254-compose-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7254-compose-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json new file mode 100644 index 000000000..036d9836e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7255-compose-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7255-compose-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json new file mode 100644 index 000000000..0b5993d32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7256-readable-compose.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7256-readable-compose.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json new file mode 100644 index 000000000..79354d9ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7257-pipeline-generator-destination.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7257-pipeline-generator-destination.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json new file mode 100644 index 000000000..313c9f19e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7258-pipeline-generator-destination.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7258-pipeline-generator-destination.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json new file mode 100644 index 000000000..6fc082ea1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7259-promises-pipeline-stage-options.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7259-promises-pipeline-stage-options.cjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 631750cdf..3d22b2360 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 0e81033ad..909412bc7 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index a3ac43cc0..9bd5abf8c 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v27(void) {} +void scr_runtime_abi_v28(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index fea020d16..35332d0ca 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v27(void); +void scr_runtime_abi_v28(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 691b6af8e..75debe615 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -1534,8 +1534,21 @@ static bool scr_from_check_hwm(double hwm) { * highWaterMark 1 for iterables, the mode's default for the string / * Buffer case or a nullish option, or the option's value. +1, or NULL with the error * pending. Moves f. */ +/* from.js's `new Readable(...)`: a Readable, or for Duplex.from (duplexify.js's + * from(Duplexify, body, { objectMode: true, writable: false })) a Duplexify + * whose writable side is off. */ +static ScrStream *scr_from_new(bool duplex, double hwm, ScrClosure *read, ScrStreamReadInv read_inv, + ScrClosure *destroy, ScrStreamErrInv destroy_inv) { + if (!duplex) return scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, destroy_inv); + ScrStream *d = scr_stream_new_duplex(hwm, hwm, true, true, true, true, false, read, read_inv, NULL, + NULL, NULL, NULL, destroy, destroy_inv); + scr_stream_set_object_mode(d, false, true); + d->cls = "Duplexify"; + return d; +} + static ScrStream *scr_from_stream(ScrFromSrc *f, ScrStreamReadInv read_inv, bool object_mode, - int hwm_kind, double hwm, bool strings) { + int hwm_kind, double hwm, bool strings, bool duplex) { if (hwm_kind == SCR_FROM_HWM_SET && !scr_from_check_hwm(hwm)) { scr_from_src_release(f); return NULL; @@ -1547,8 +1560,8 @@ static ScrStream *scr_from_stream(ScrFromSrc *f, ScrStreamReadInv read_inv, bool scr_box_set_ref(read->caps[0], f); /* moves */ ScrClosure *destroy = NULL; if (!f->single) destroy = scr_closure_new((void *)&scr_from_destroy_inv, 0); - ScrStream *s = scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, - destroy ? &scr_from_destroy_inv : NULL); + ScrStream *s = scr_from_new(duplex, hwm, read, read_inv, destroy, + destroy ? &scr_from_destroy_inv : NULL); ScrStreamState *st = s->st; if (object_mode) { st->r.object_entries = true; @@ -1575,7 +1588,7 @@ ScrStream *scr_stream_new_from_readable(ScrClosure *read, ScrStreamReadInv read_ bool object_mode = (bits & SCR_FROM_BYTE_MODE) == 0; if (bits & SCR_FROM_DEFAULT_HWM) hwm = object_mode ? 16 : -1; else if (!(bits & SCR_FROM_HAS_HWM)) hwm = 1; - ScrStream *s = scr_stream_new_readable(hwm, true, true, read, read_inv, destroy, destroy_inv); + ScrStream *s = scr_from_new((bits & SCR_FROM_DUPLEX) != 0, hwm, read, read_inv, destroy, destroy_inv); if (object_mode) s->st->r.object_entries = true; return s; } @@ -1590,7 +1603,7 @@ ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm) { f->single = (bits & SCR_FROM_SINGLE) != 0; return scr_from_stream(f, &scr_from_read_inv, (bits & SCR_FROM_BYTE_MODE) == 0, (bits & SCR_FROM_HAS_HWM) ? SCR_FROM_HWM_SET : SCR_FROM_HWM_FROM, hwm, - f->strings); + f->strings, (bits & SCR_FROM_DUPLEX) != 0); } /* The JS lane: one checked-dynamic value into the stream. null is @@ -1654,7 +1667,7 @@ static void scr_from_read_dyn_inv(ScrClosure *cb, ScrStream *s, double size) { scr_from_src_release(f); } -ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { +static ScrStream *scr_from_dyn_ex(ScrDyn *src, ScrDyn *opts, bool duplex) { bool single = src->kind == SCR_DYN_STR || src->kind == SCR_DYN_BYTES; ScrDyn *iter = NULL; /* from.js's iterator, for sources other than arrays */ int kind = 0; @@ -1666,7 +1679,14 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { if (kind < 0) return NULL; } if (!single && kind == 0) { - scr_dyn_arg_type_fail("iterable", "an instance of Iterable", src); + if (duplex) { + scr_dyn_arg_type_fail("body", + "of type function or an instance of Blob, ReadableStream, WritableStream, " + "Stream, Iterable, AsyncIterable, or Promise or { readable, writable } pair", + src); + } else { + scr_dyn_arg_type_fail("iterable", "an instance of Iterable", src); + } return NULL; } bool object_mode = true; @@ -1713,7 +1733,7 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { } } if (iter) { - double flags = (object_mode ? 0 : SCR_FROM_BYTE_MODE) | + double flags = (object_mode ? 0 : SCR_FROM_BYTE_MODE) | (duplex ? SCR_FROM_DUPLEX : 0) | (hwm_kind == SCR_FROM_HWM_SET ? SCR_FROM_HAS_HWM : hwm_kind == SCR_FROM_HWM_DEFAULT ? SCR_FROM_DEFAULT_HWM : 0); @@ -1724,7 +1744,39 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { f->rc = 1; f->dyn = scr_dyn_retain(src); f->single = single; - return scr_from_stream(f, &scr_from_read_dyn_inv, object_mode, hwm_kind, hwm, false); + return scr_from_stream(f, &scr_from_read_dyn_inv, object_mode, hwm_kind, hwm, false, duplex); +} + +ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { + return scr_from_dyn_ex(src, opts, false); +} + +/* Duplex.from(body) over a checked-dynamic body (duplexify.js): a native + * Duplex answers itself, an iterable reads as Duplex.from does, and the + * other bodies Node accepts (a lone Readable or Writable, a function, a + * promise, a { readable, writable } pair) are not supported yet. */ +static ScrStream *scr_stream_boxed(const ScrDyn *value); + +ScrStream *scr_stream_duplex_from_dyn(ScrDyn *body) { + ScrStream *native = scr_stream_of_handle(body); /* +1 */ + if (!native) native = scr_stream_boxed(body); /* +1 */ + if (native && native->st->has_r && native->st->has_w) return native; + bool unsupported = native != NULL; + if (native) scr_stream_release(native); + if (!unsupported && (body->kind == SCR_DYN_FUNC || body->kind == SCR_DYN_PROMISE)) unsupported = true; + if (!unsupported && body->kind == SCR_DYN_OBJ) { + static const char *const keys[] = {"then", "readable", "writable"}; + for (size_t i = 0; i < 3 && !unsupported; i++) { + ScrDyn *v = scr_dyn_obj_get(body, keys[i], strlen(keys[i])); + unsupported = v != NULL && v->kind != SCR_DYN_UNDEF; + } + } + if (unsupported) { + static const char msg[] = "Duplex.from over this body is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + return NULL; + } + return scr_from_dyn_ex(body, NULL, true); } /* ── object-mode streams for the stream operators (scr_stream_ops.c) ──── */ @@ -3695,7 +3747,9 @@ typedef ScrPromise *(*ScrPipeNextFn)(ScrClosure *, ScrGen *); typedef ScrDyn *(*ScrPipeReadFn)(ScrClosure *, void *); typedef ScrDyn *(*ScrPipeBoxFn)(ScrClosure *, ScrPromise *); -enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_PROMISE = 2 }; +enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_GEN, SCR_PIPE_DEST_PROMISE }; +/* flags bit 7: the callback is typed code's (stream.pipelineStagesDyn) */ +enum { SCR_PIPE_TYPED_CB = 128 }; typedef struct { size_t count; /* the function stages */ @@ -3707,8 +3761,9 @@ typedef struct { int dest; /* the last stage: SCR_PIPE_DEST_* */ unsigned opts_mask; /* bit i: stages[i] takes the { signal } argument */ ScrDyn *signal; /* the pipeline's AbortSignal when opts_mask != 0 */ - /* (next, read, return) for each generator pumped into a stream, in - * pipeline order, or NULL */ + /* (next, read, return) for each generator pumped into a stream (an + * async generator destination's PassThrough included), in pipeline + * order, or NULL */ ScrClosure **helpers; ScrClosure *box; /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise */ } ScrPipeStages; @@ -4043,7 +4098,7 @@ static void scr_pipe_dest_entry(ScrFiber *fiber, void *arg) { ctx->value = v; if (ctx->value_p) scr_promise_release(ctx->value_p); ctx->value_p = scr_promise_retain(d.typed); - if (pt && v && (v->kind == SCR_DYN_STR || v->kind == SCR_DYN_BYTES)) { + if (pt && v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { scr_stream_write_dyn(pt, v, NULL); if (scr_exc_pending()) scr_exc_clear(); } @@ -4147,17 +4202,22 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ret_gen = out; continue; } - /* The destination function: pipeline() answers a PassThrough proxy - * that the promise's value feeds. Node's is in object mode; the - * frontend refuses programs that read it, so a byte-mode - * PassThrough plays its part in the pipeline's bookkeeping. */ + /* The destination function: pipeline() answers an object-mode + * PassThrough proxy that the promise's value or the generator's + * values feed. */ ScrDyn *pt_opts = scr_dyn_new_obj(); + scr_dyn_obj_set(pt_opts, "objectMode", 10, scr_dyn_new_bool(true)); ScrStream *pt = scr_stream_new_passthrough_dyn(pt_opts); scr_dyn_release(pt_opts); size_t pi = ctx->n++; ctx->streams[pi] = pt; ctx->finish_count++; - scr_pipe_dest_promise(ctx, out, fs->box, pi, end); + if (fs->dest == SCR_PIPE_DEST_GEN) { + /* pumpToNode(ret, pt, finish, { end }) */ + scr_pump_start(ctx, out, fs->helpers + 3 * h++, pt, end); + } else { + scr_pipe_dest_promise(ctx, out, fs->box, pi, end); + } /* destroyer(pt, false, true), with its cleanup as the last stream's */ ctx->destroyer[pi] = true; scr_pipe_on(pt, "close", scr_pipe_closure((void *)&scr_pipe_destroyer_close_l, ctx, pi), @@ -4245,12 +4305,14 @@ static ScrPipeStages scr_pipe_stages_of(double count, ScrClosure **stages, doubl } /* callback(error, value) through a checked-dynamic callback: Node passes - * both arguments. */ -static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPromise *value_p) { - (void)value_p; + * both arguments. A typed callback (SCR_PIPE_TYPED_CB) takes null for no + * error, as the typed callback forms do. */ +static void scr_pipe_dyn_value_call(ScrClosure *cb, ScrError *err, ScrDyn *value, bool typed) { ScrDyn *fn = scr_stream_dynopt_target(cb); - ScrDyn *args[2] = {err ? scr_dyn_from_error(err) : scr_dyn_retain(scr_dyn_undefined()), + ScrDyn *none = typed ? scr_dyn_new_null() : scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *args[2] = {err ? scr_dyn_from_error(err) : none, value && !err ? scr_dyn_retain(value) : scr_dyn_retain(scr_dyn_undefined())}; + if (err) scr_dyn_release(none); ScrDyn *r = scr_dyn_call(fn, args, 2, "callback"); scr_dyn_release(args[0]); scr_dyn_release(args[1]); @@ -4258,6 +4320,17 @@ static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, if (r != NULL) scr_dyn_release(r); } +static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPromise *value_p) { + (void)value_p; + scr_pipe_dyn_value_call(cb, err, value, false); +} + +static void scr_pipe_dyn_value_typed_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, + ScrPromise *value_p) { + (void)value_p; + scr_pipe_dyn_value_call(cb, err, value, true); +} + /* pipeline(...) with function stages (see ScrPipeStages): positions * marks the function stages among the n + count stages, flags bits 1-2 * give the destination kind, and mask names the stages taking @@ -4279,7 +4352,9 @@ ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); ScrClosure *c = scr_stream_dynopt_clo(cb, (void *)&scr_stream_finished_dyn_inv); /* a function destination's value reaches the callback */ - ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_pipe_dyn_value_inv : NULL; + ScrPipeValInv vi = fs.dest == SCR_PIPE_DEST_STREAM ? NULL + : ((int)flags & SCR_PIPE_TYPED_CB) ? &scr_pipe_dyn_value_typed_inv + : &scr_pipe_dyn_value_inv; return scr_stream_pipeline_ex((size_t)n, streams, c, &scr_stream_finished_dyn_inv, true, NULL, NULL, &fs, abort, vi); } @@ -4333,7 +4408,7 @@ static void *scr_compose_retain_v(void *p) { static void scr_compose_release_v(void *p) { ScrCompose *c = p; if (--c->rc > 0) return; - scr_stream_release(c->head); + if (c->head) scr_stream_release(c->head); scr_stream_release(c->tail); free(c); } @@ -4393,7 +4468,9 @@ static void scr_compose_destroyed(ScrCompose *c, ScrError *err) { /* d._write: head.write(chunk) ? callback() : (ondrain = callback) */ static void scr_compose_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { ScrCompose *c = scr_box_get_ref(cb->caps[0]); - if (scr_stream_write(c->head, chunk, NULL)) { + bool ok = scr_stream_writes_values(s) ? scr_stream_write_dyn(c->head, (ScrDyn *)chunk, NULL) + : scr_stream_write(c->head, chunk, NULL); + if (ok) { if (!scr_exc_pending()) scr_stream_write_done(s, NULL); } else { c->ondrain = true; @@ -4524,43 +4601,54 @@ static void scr_compose_arg_value(size_t i, const char *what, ScrStream *s) { scr_throw_error_msg_code(SCR_ERR_TYPE, m, (size_t)n, "ERR_INVALID_ARG_VALUE"); } -ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { - size_t n = (size_t)n_d; - if (n == 0) { +/* compose(...streams) over n streams and fs's function stages: the stream + * at pipeline position j must be readable unless it is last and writable + * unless it is first. A first stage that is not a stream (an iterable or + * source function) leaves the Duplex without a writable side. */ +static ScrStream *scr_stream_compose_impl(size_t n, ScrStream **streams, const ScrPipeStages *fs, + ScrClosure *abort) { + size_t total = n + (fs ? fs->count : 0); + if (total == 0) { static const char msg[] = "The \"streams\" argument must be specified"; scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_MISSING_ARGS"); return NULL; } - if (n == 1) return scr_stream_retain(streams[0]); /* Duplex.from(duplex) */ - for (size_t i = 0; i < n; i++) { - if (i + 1 < n && !scr_stream_prop(streams[i], "readable")) { - scr_compose_arg_value(i, "readable", streams[i]); + if (total == 1 && n == 1) return scr_stream_retain(streams[0]); /* Duplex.from(duplex) */ + for (size_t j = 0, i = 0; j < total; j++) { + if (fs && j < 64 && ((fs->positions >> j) & 1u)) continue; + if (j + 1 < total && !scr_stream_prop(streams[i], "readable")) { + scr_compose_arg_value(j, "readable", streams[i]); return NULL; } - if (i > 0 && !scr_stream_prop(streams[i], "writable")) { - scr_compose_arg_value(i, "writable", streams[i]); + if (j > 0 && !scr_stream_prop(streams[i], "writable")) { + scr_compose_arg_value(j, "writable", streams[i]); return NULL; } + i++; } + bool stage_head = fs && (fs->positions & 1u); ScrCompose *c = calloc(1, sizeof *c); if (!c) scr_stream_oom(); c->rc = 1; - c->head = scr_stream_retain(streams[0]); + c->head = stage_head ? NULL : scr_stream_retain(streams[0]); c->tail = scr_stream_retain(streams[n - 1]); c->onclose = SCR_COMPOSE_RUNNING; ScrStream *tail = scr_stream_pipeline_ex(n, streams, scr_compose_closure((void *)&scr_compose_finished, c), - &scr_compose_finished, true, NULL, NULL, NULL, NULL, + &scr_compose_finished, true, NULL, NULL, fs, abort, NULL); if (!tail) { scr_compose_release_v(c); return NULL; } scr_stream_release(tail); - c->writable = scr_stream_prop(c->head, "writable") != 0; + c->writable = c->head && scr_stream_prop(c->head, "writable") != 0; c->readable = scr_stream_prop(c->tail, "readable") != 0; + /* readableObjectMode: !!tail?.readableObjectMode, writableObjectMode: + * !!head?.writableObjectMode */ bool object_mode = c->tail->st->has_r && c->tail->st->r.object_entries; + bool w_object = c->head && c->head->st->has_w && c->head->st->w.object_mode; ScrStream *d = scr_stream_new_duplex( - object_mode ? 16 : -1, -1, true, true, true, c->readable, c->writable, + object_mode ? 16 : -1, w_object ? 16 : -1, true, true, true, c->readable, c->writable, c->readable ? scr_compose_closure((void *)&scr_compose_read_inv, c) : NULL, c->readable ? &scr_compose_read_inv : NULL, c->writable ? scr_compose_closure((void *)&scr_compose_write_inv, c) : NULL, @@ -4569,6 +4657,7 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { c->writable ? &scr_compose_final_inv : NULL, scr_compose_closure((void *)&scr_compose_destroy_inv, c), &scr_compose_destroy_inv); if (object_mode) d->st->r.object_entries = true; + if (w_object) scr_stream_set_object_mode(d, false, true); c->d = scr_stream_retain(d); if (c->writable) { scr_compose_on(c->head, "drain", &c->drain_l, (void *)&scr_compose_drain_l, c, scr_ee_inv_fixed0); @@ -4590,6 +4679,19 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { return d; } +ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { + return scr_stream_compose_impl((size_t)n_d, streams, NULL, NULL); +} + +/* compose(...stages) with function stages among the streams (the + * pipeline stage layout: scr_stream_pipeline_stages). */ +ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count, ScrClosure **stages, + double positions, double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal, ScrClosure *abort) { + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); + return scr_stream_compose_impl((size_t)n, streams, &fs, abort); +} + /* ── node:stream/promises ───────────────────────────────────────────── * The promise forms ride the callback machinery above with a settling * watcher: caps[0] boxes the pending promise; the terminal status @@ -4666,6 +4768,24 @@ ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, return p; } +/* The options' signal: NULL for a nullish one; false (p rejected with + * ERR_INVALID_ARG_TYPE) for a value that is not an AbortSignal. */ +static bool scr_sp_opts_signal(ScrPromise *p, ScrDyn **signal) { + if (*signal && scr_dyn_is_nullish(*signal)) *signal = NULL; + if (!*signal) return true; + ScrDyn *aborted = NULL; + if ((*signal)->kind == SCR_DYN_OBJ || (*signal)->kind == SCR_DYN_HANDLE) { + aborted = scr_stream_signal_prop(*signal, "aborted"); + } + if (!aborted) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", *signal); + scr_promise_reject_pending(p); + return false; + } + scr_dyn_release(aborted); + return true; +} + /* pipeline(...streams, { signal, end }): end false leaves the destination * open (the promise settles once the stages before it finish); signal is * NULL or borrowed — a value that is not an AbortSignal rejects with @@ -4673,19 +4793,7 @@ ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, ScrAbortAddFn add) { ScrPromise *p = scr_promise_new(); - if (signal && scr_dyn_is_nullish(signal)) signal = NULL; - if (signal) { - ScrDyn *aborted = NULL; - if (signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_HANDLE) { - aborted = scr_stream_signal_prop(signal, "aborted"); - } - if (!aborted) { - scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); - scr_promise_reject_pending(p); - return p; - } - scr_dyn_release(aborted); - } + if (!scr_sp_opts_signal(p, &signal)) return p; ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, end, signal, add, NULL, NULL, NULL); if (dst) scr_stream_release(dst); @@ -4693,6 +4801,25 @@ ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn return p; } +/* scr_sp_pipeline_stages with the options argument: end and the + * options' signal (outer, NULL or borrowed) as scr_sp_pipeline_opts + * takes them. The stages' signal is the pipeline's own. */ +ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, bool end, + ScrDyn *outer, ScrAbortAddFn add) { + ScrPromise *p = scr_promise_new(); + if (!scr_sp_opts_signal(p, &outer)) return p; + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); + ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_sp_value_inv : NULL; + ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, + end, outer, add, &fs, abort, vi); + if (dst) scr_stream_release(dst); + else scr_promise_reject_pending(p); + return p; +} + /* ── node:stream/consumers ──────────────────────────────────────────── * text/json/buffer/arrayBuffer over the readable machinery: a native 'data' listener * accumulates every chunk (Buffer chunks as-is; string chunks — an diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index c78a03e1a..0e0416e05 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -225,6 +225,9 @@ ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double coun double mask, ScrClosure **helpers, ScrClosure *box, struct ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, ScrStreamErrInv inv); +ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count, ScrClosure **stages, + double positions, double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal, ScrClosure *abort); ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, ScrClosure **stages, double positions, double flags, double mask, ScrClosure **helpers, ScrClosure *box, @@ -331,6 +334,7 @@ enum { SCR_FROM_BYTE_MODE = 4, SCR_FROM_HAS_HWM = 8, SCR_FROM_DEFAULT_HWM = 16, /* a nullish highWaterMark: the mode's default */ + SCR_FROM_DUPLEX = 32, /* Duplex.from: a Duplexify with its writable side off */ }; ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm); /* Readable.from(iterable, options) over a checked-dynamic source: a @@ -447,6 +451,12 @@ ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, * ERR_INVALID_ARG_TYPE. Streams borrowed. */ ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, ScrAbortAddFn add); +/* scr_sp_pipeline_stages with the options' end and signal (outer). */ +ScrPromise *scr_sp_pipeline_stages_opts(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, bool end, + ScrDyn *outer, ScrAbortAddFn add); double scr_stream_prop(ScrStream *s, const char *name); ScrError *scr_stream_errored(ScrStream *s); /* +1 or NULL */ @@ -519,6 +529,14 @@ void scr_stream_set_writev_concat(ScrStream *s); void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +/* Duplex.from (duplexify.js): over an async generator function (the + * compiled stage takes the source generator that the compiled source + * makes from next() and ack(); all borrowed, helpers too), and over a + * checked-dynamic body (borrowed). +1 each. The typed iterable bodies take + * the Readable.from builders with SCR_FROM_DUPLEX. */ +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr); +ScrStream *scr_stream_duplex_from_dyn(struct ScrDyn *body); /* decodeStrings (false keeps string writes as strings), the writable * defaultEncoding option (a known encoding, borrowed), and * setDefaultEncoding (validated; answers the stream +1). The chaining diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index 8fd220168..1db90bc5a 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -2029,6 +2029,238 @@ ScrStream *scr_stream_from_async_iter(ScrDyn *iter, double flags, double hwm) { return scr_stream_from_dyn_iter(scr_dyn_retain(iter), true, flags, hwm); } +/* ── Duplex.from (duplexify.js) ───────────────────────────────────── + * Duplex.from(asyncGeneratorFunction): fromAsyncGen hands the function a + * source generator that the writes and the final feed through a deferred + * promise, and from(Duplexify, value, { objectMode: true, write, final, + * destroy }) reads what the function yields. The source generator is + * compiled (the frontend's Duplex.from source helper): + * + * while (true) { const v = await next(); ack(); if (v is done) return; yield v; } + * + * next() answers the current deferred, a write or the final settles it + * (the chunk, or the done marker), and ack() is fromAsyncGen's + * `process.nextTick(cb)` followed by a fresh deferred. */ + +enum { CHAN_NONE, CHAN_WRITE, CHAN_FINAL, CHAN_ABORT }; + +typedef struct OpChan OpChan; +struct OpChan { + OpObj base; + ScrStream *d; /* +1, dropped at destroy (or exit) */ + ScrPromise *promise; /* +1: the deferred next() answers, NULL once taken */ + ScrPromise *resolve; /* +1: the deferred a write or the final settles, NULL once settled */ + int settled; /* CHAN_*: what settled the deferred, for ack() */ + bool aborted; /* destroy ran (fromAsyncGen's ac.abort()) */ + OpChan *live_prev, *live_next; +}; + +static SCR_TL OpChan *op_chan_live = NULL; + +static void op_chan_drop(OpObj *o) { + OpChan *c = (OpChan *)o; + if (c->d) scr_stream_release(c->d); + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + free(c); +} + +/* createDeferredPromise() */ +static void op_chan_defer(OpChan *c) { + ScrPromise *p = scr_promise_new(); + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + c->promise = p; + c->resolve = scr_promise_retain(p); +} + +/* Unlinks the channel from the live list and drops its stream and its + * deferred (a source still awaiting it stays pending, as in Node). */ +static void op_chan_retire(OpChan *c) { + if (c->live_prev) c->live_prev->live_next = c->live_next; + else if (op_chan_live == c) op_chan_live = c->live_next; + else return; /* already retired */ + if (c->live_next) c->live_next->live_prev = c->live_prev; + c->live_prev = c->live_next = NULL; + ScrStream *d = c->d; + c->d = NULL; + if (c->promise) scr_promise_release(c->promise); + if (c->resolve) scr_promise_release(c->resolve); + c->promise = c->resolve = NULL; + if (d) scr_stream_release(d); + op_unref(c); /* the live list's reference */ +} + +static void op_chan_atexit(void) { + while (op_chan_live) op_chan_retire(op_chan_live); +} + +static ScrClosure *op_chan_closure(void *fn, OpChan *c) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&op_ref, &op_unref, NULL); + scr_box_set_ref(cl->caps[0], op_ref(c)); + return cl; +} + +static OpChan *op_chan_of(ScrClosure *cb) { return scr_box_get_ref(cb->caps[0]); /* +1 */ } + +/* process.nextTick(cb): the write's or the final's completion. */ +static void op_chan_done_tick(ScrClosure *job) { + ScrStream *d = scr_box_get_ref(job->caps[0]); + bool final = scr_box_get_bool(job->caps[1]); + if (final) scr_stream_final_done(d, NULL); + else scr_stream_write_done(d, NULL); + scr_stream_release(d); +} + +/* The source's `await _promise`: the current deferred (+1). */ +static ScrPromise *op_chan_next_inv(ScrClosure *cb) { + OpChan *c = op_chan_of(cb); + ScrPromise *p = c->promise; + c->promise = NULL; + op_unref(c); + return p ? p : scr_promise_new(); +} + +static ScrError *op_chan_abort_error(void) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + return e; +} + +/* After the source's await: process.nextTick(cb), then (unless done) the + * aborted check and a fresh deferred. */ +static void op_chan_ack_inv(ScrClosure *cb) { + OpChan *c = op_chan_of(cb); + int settled = c->settled; + c->settled = CHAN_NONE; + if (c->d && (settled == CHAN_WRITE || settled == CHAN_FINAL)) { + ScrClosure *tick = scr_closure_new((void *)&op_chan_done_tick, 2); + tick->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(tick->caps[0], scr_stream_retain(c->d)); + tick->caps[1] = scr_box_new(SCR_BOX_BOOL); + scr_box_set_bool(tick->caps[1], settled == CHAN_FINAL); + scr_next_tick(tick); + } + if (settled != CHAN_FINAL && settled != CHAN_ABORT) { + if (c->aborted) { + ScrError *e = op_chan_abort_error(); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); + } else if (c->d) { + op_chan_defer(c); + } + } + op_unref(c); +} + +/* `_resolve(value)`: the write's chunk or the final's done marker. */ +static void op_chan_settle(OpChan *c, ScrDyn *v /*moves*/, int kind) { + ScrPromise *p = c->resolve; + c->resolve = NULL; + if (!p) { + scr_dyn_release(v); + return; + } + c->settled = kind; + scr_promise_fulfill_ref(p, v, scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_promise_release(p); +} + +/* Duplexify's write(chunk, encoding, cb) */ +static void op_chan_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + OpChan *c = op_chan_of(cb); + op_chan_settle(c, scr_stream_chunk_value(s, chunk), CHAN_WRITE); + op_unref(c); +} + +/* Duplexify's final(cb) */ +static void op_chan_final_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + OpChan *c = op_chan_of(cb); + op_chan_settle(c, scr_stream_from_done(), CHAN_FINAL); + op_unref(c); +} + +/* destroy: fromAsyncGen's (ac.abort(); a source waiting for a write or + * the final is unblocked with the done marker and a no-op callback; + * cb(err)), then from()'s close of the function's iterator. caps: the + * channel and the from() destroy closure. */ +static void op_dup_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + OpChan *c = scr_box_get_ref(cb->caps[0]); + ScrClosure *from = scr_box_get_ref(cb->caps[1]); + c->aborted = true; + if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); + op_chan_retire(c); + op_unref(c); + op_from_destroy_inv(from, s, err); + scr_closure_release(from); +} + +typedef ScrGen *(*OpSourceFn)(ScrClosure *, ScrClosure *, ScrClosure *); +typedef ScrGen *(*OpStageFn)(ScrClosure *, ScrGen *); + +/* Duplex.from(fn) for an async generator function: the compiled stage + * (borrowed) takes the source generator and answers its own; source + * (borrowed) makes the source generator from next() and ack(); the + * compiler's helpers over the stage's generator are borrowed. +1, or + * NULL with the function's throw pending. */ +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr) { + op_sentinels_init(); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(op_chan_atexit); + } + OpChan *c = op_alloc(sizeof *c); + c->base.rc = 1; + c->base.drop = &op_chan_drop; + op_chan_defer(c); + ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), + op_chan_closure((void *)&op_chan_ack_inv, c)); + ScrGen *g = scr_exc_pending() ? NULL : ((OpStageFn)stage->fn)(stage, src); /* src moves in */ + if (scr_exc_pending()) { + if (g) scr_gen_release(g); + op_unref(c); + return NULL; + } + OpFrom *f = op_alloc(sizeof *f); + f->base.rc = 1; + f->base.drop = &op_from_drop; + f->kind = FROM_ASYNC; + f->gen = g; + f->next = scr_closure_retain(next); + f->read = scr_closure_retain(read); + f->ret = scr_closure_retain(ret); + f->thr = scr_closure_retain(thr); + ScrClosure *destroy = scr_closure_new((void *)&op_dup_destroy_inv, 2); + destroy->caps[0] = scr_box_new_obj(&op_ref, &op_unref, NULL); + scr_box_set_ref(destroy->caps[0], op_ref(c)); + destroy->caps[1] = scr_box_new_obj(&scr_closure_retain_v, &scr_closure_release_v, &scr_closure_trace_v); + scr_box_set_ref(destroy->caps[1], op_from_closure((void *)&op_from_destroy_inv, f)); + /* Duplexify({ objectMode: true, highWaterMark: 1, write, final, destroy }) */ + ScrStream *d = scr_stream_new_duplex(1, 1, true, true, true, true, true, + op_from_closure((void *)&op_from_read_inv, f), &op_from_read_inv, + op_chan_closure((void *)&op_chan_write_inv, c), &op_chan_write_inv, + op_chan_closure((void *)&op_chan_final_inv, c), &op_chan_final_inv, + destroy, &op_dup_destroy_inv); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + f->out = d; + op_unref(f); + c->d = scr_stream_retain(d); + c->live_next = op_chan_live; + if (op_chan_live) op_chan_live->live_prev = c; + op_chan_live = c; /* the reference c was created with */ + return d; +} + /* ── the promise operators ─────────────────────────────────────────── */ enum { PK_DYN, PK_F64, PK_BOOL, PK_STR, PK_VOID }; diff --git a/tests/corpus/7252-duplex-from-generator.ts b/tests/corpus/7252-duplex-from-generator.ts new file mode 100644 index 000000000..488748021 --- /dev/null +++ b/tests/corpus/7252-duplex-from-generator.ts @@ -0,0 +1,69 @@ +// Duplex.from over an async generator function in typed code: the +// generator reads what is written to the Duplex and its values are the +// readable side. Both sides are in object mode with a highWaterMark of +// 1; each write's callback runs once the generator asks for the next +// value, and end() reaches the generator as the end of its source. +import { Duplex } from "node:stream"; + +const log: string[] = []; + +async function* numbered(source: AsyncIterable): AsyncGenerator { + let n = 0; + for await (const chunk of source) { + n++; + log.push(`gen got ${String(chunk)}`); + yield `${n}:${String(chunk)}`; + } + log.push("gen done"); + yield `total ${n}`; +} + +async function collect(d: Duplex): Promise { + const out: string[] = []; + for await (const chunk of d) out.push(String(chunk)); + return out; +} + +async function basic(): Promise { + const d = Duplex.from(numbered); + log.push(`sides ${d.readable} ${d.writable}`); + log.push(`object mode ${d.readableObjectMode} ${d.writableObjectMode}`); + log.push(`hwm ${d.readableHighWaterMark} ${d.writableHighWaterMark}`); + d.write("a", () => log.push("write a done")); + d.write("b", () => log.push("write b done")); + d.end("c", () => log.push("end done")); + const out = await collect(d); + log.push(`read ${out.join(",")}`); + log.push(`after ${d.readableEnded} ${d.writableFinished} ${d.destroyed}`); +} + +async function values(): Promise { + // object-mode writes keep their values + const d = Duplex.from(async function* (source: AsyncIterable): AsyncGenerator { + for await (const v of source) yield typeof v + " " + JSON.stringify(v); + }); + d.write({ k: 1 }); + d.write([2, 3]); + d.write(4); + d.end(); + log.push(`values ${(await collect(d)).join(" | ")}`); +} + +async function earlyDestroy(): Promise { + const d = Duplex.from(numbered); + d.on("error", (e: Error) => log.push(`error ${e.message}`)); + d.on("close", () => log.push(`close ${d.destroyed}`)); + d.write("x"); + await new Promise((resolve) => setTimeout(resolve, 5)); + d.destroy(new Error("stop")); + await new Promise((resolve) => setTimeout(resolve, 5)); +} + +async function main(): Promise { + await basic(); + await values(); + await earlyDestroy(); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7253-duplex-from.mjs b/tests/corpus/7253-duplex-from.mjs new file mode 100644 index 000000000..14556348a --- /dev/null +++ b/tests/corpus/7253-duplex-from.mjs @@ -0,0 +1,48 @@ +// Duplex.from in JavaScript: an async generator function, an array, a +// generator object and an async generator object. Over an iterable the +// result is read-only (writable false); over a function it reads what is +// written. The log records the order of the write callbacks, the reads +// and the stream events against each other. +import { Duplex } from "node:stream"; + +const log = []; + +async function* echo(source) { + for await (const chunk of source) { + log.push(`echo ${chunk}`); + yield chunk.toUpperCase(); + } +} + +const fromFn = Duplex.from(echo); +fromFn.on("finish", () => log.push("fn finish")); +fromFn.on("end", () => log.push("fn end")); +fromFn.on("close", () => log.push("fn close")); +fromFn.write("one", () => log.push("fn write one cb")); +fromFn.write("two", () => log.push("fn write two cb")); +fromFn.end(() => log.push("fn end cb")); +for await (const chunk of fromFn) log.push(`fn read ${chunk}`); + +const fromArray = Duplex.from(["p", "q"]); +log.push(`array ${fromArray.readable} ${fromArray.writable} ${fromArray.readableObjectMode}`); +for await (const chunk of fromArray) log.push(`array ${chunk}`); + +function* gen() { + yield "g1"; + yield "g2"; +} +const fromGen = Duplex.from(gen()); +log.push(`gen ${fromGen.writable}`); +for await (const chunk of fromGen) log.push(`gen ${chunk}`); + +async function* agen() { + yield 1; + await null; + yield 2; +} +const fromAgen = Duplex.from(agen()); +fromAgen.on("data", (v) => log.push(`agen data ${v}`)); +await new Promise((resolve) => fromAgen.on("end", resolve)); +log.push(`agen ended ${fromAgen.readableEnded}`); + +console.log(log.join("\n")); diff --git a/tests/corpus/7254-compose-functions.ts b/tests/corpus/7254-compose-functions.ts new file mode 100644 index 000000000..4a2ba7189 --- /dev/null +++ b/tests/corpus/7254-compose-functions.ts @@ -0,0 +1,56 @@ +// stream.compose with async generator function stages in typed code: a +// function at either end becomes Duplex.from of it, functions between +// streams run as pipeline stages, and one stage is Duplex.from of it. +// The composed Duplex writes to the first stage and reads from the last; +// its object modes follow the first stage's writable side and the last +// stage's readable side. +import { Duplex, PassThrough, Transform, compose } from "node:stream"; + +const log: string[] = []; + +async function* upper(source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield String(chunk).toUpperCase(); +} + +async function* numbered(source: AsyncIterable): AsyncGenerator { + let n = 0; + for await (const chunk of source) yield `${++n}.${String(chunk)}`; +} + +async function drain(name: string, d: Duplex, chunks: string[]): Promise { + log.push(`${name} r=${d.readable} w=${d.writable} ro=${d.readableObjectMode} wo=${d.writableObjectMode}`); + const out: string[] = []; + d.on("data", (c: unknown) => out.push(String(c))); + const ended = new Promise((resolve) => d.on("end", () => resolve())); + for (const c of chunks) d.write(c); + d.end(); + await ended; + log.push(`${name} ${out.join(" ")}`); +} + +async function main(): Promise { + // functions only: Duplex.from at both ends + await drain("fns", compose(upper, numbered), ["a", "b"]); + // a function between two object-mode streams + await drain( + "middle", + compose(new PassThrough({ objectMode: true }), upper, new PassThrough({ objectMode: true })), + ["c", "d"], + ); + // a byte stream before a function: the Duplex writes bytes + const bytes = compose(new PassThrough(), numbered); + await drain("bytes", bytes, ["e"]); + // a function before a Transform + const tail = new Transform({ + objectMode: true, + transform(chunk: unknown, _enc: string, cb: (err?: Error | null, data?: unknown) => void): void { + cb(null, `<${String(chunk)}>`); + }, + }); + await drain("transform", compose(upper, tail), ["f", "g"]); + // one stage + await drain("single", compose(numbered), ["h", "i", "j"]); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7255-compose-stages.mjs b/tests/corpus/7255-compose-stages.mjs new file mode 100644 index 000000000..ef6542ece --- /dev/null +++ b/tests/corpus/7255-compose-stages.mjs @@ -0,0 +1,55 @@ +// stream.compose in JavaScript with an iterable or generator first stage +// (the Duplex is not writable), a stage that throws (the Duplex errors), +// a destroyed composition (the stages see an AbortError), and compose() +// with no stages (ERR_MISSING_ARGS). +import { PassThrough, compose } from "node:stream"; + +const log = []; + +const double = async function* (source) { + for await (const n of source) yield n * 2; +}; + +const fromArray = compose([1, 2, 3], double); +log.push(`array r=${fromArray.readable} w=${fromArray.writable}`); +for await (const n of fromArray) log.push(`array ${n}`); + +function* counter() { + yield 10; + yield 20; +} +const fromGen = compose(counter(), double, async function* (source) { + for await (const n of source) yield `n=${n}`; +}); +for await (const s of fromGen) log.push(`gen ${s}`); + +const failing = compose(new PassThrough({ objectMode: true }), async function* (source) { + for await (const chunk of source) { + if (chunk === "stop") throw new Error("stage failed on " + chunk); + yield chunk; + } +}, new PassThrough({ objectMode: true })); +failing.on("data", (c) => log.push(`failing data ${c}`)); +failing.on("error", (e) => log.push(`failing error ${e.message}`)); +failing.on("close", () => log.push(`failing close ${failing.destroyed}`)); +failing.write("go"); +failing.write("stop"); +await new Promise((resolve) => failing.on("close", resolve)); + +const stopped = compose(new PassThrough({ objectMode: true }), double); +stopped.on("error", (e) => log.push(`stopped error ${e.name} ${e.code}`)); +stopped.on("close", () => log.push("stopped close")); +stopped.write(4); +stopped.once("data", (n) => { + log.push(`stopped data ${n}`); + stopped.destroy(); +}); +await new Promise((resolve) => stopped.on("close", resolve)); + +try { + compose(); +} catch (e) { + log.push(`empty ${e.code} ${e.message}`); +} + +console.log(log.join("\n")); diff --git a/tests/corpus/7256-readable-compose.mjs b/tests/corpus/7256-readable-compose.mjs new file mode 100644 index 000000000..a9622173e --- /dev/null +++ b/tests/corpus/7256-readable-compose.mjs @@ -0,0 +1,53 @@ +// readable.compose(stage, { signal }): compose(this, stage), with the +// signal aborting the composed Duplex. The stage may be an async +// generator function or a stream; the result reads like the stage's +// output and is not writable when the receiver heads it. +import { PassThrough, Readable, Transform } from "node:stream"; + +const log = []; + +const WORDS = ["alpha", "beta", "gamma"]; +const lengths = async function* (source) { + for await (const w of source) yield `${w}:${w.length}`; +}; + +const source = Readable.from(WORDS); +const viaFn = source.compose(lengths); +log.push(`fn r=${viaFn.readable} w=${viaFn.writable} ro=${viaFn.readableObjectMode}`); +for await (const s of viaFn) log.push(`fn ${s}`); + +const shout = new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, chunk.toUpperCase()); + }, +}); +const source2 = Readable.from(WORDS); +const viaStream = source2.compose(shout); +log.push(`stream ${(await viaStream.toArray()).join(",")}`); + +const pass = new PassThrough({ objectMode: true }); +const chained = pass.compose(lengths).compose(async function* (source) { + for await (const s of source) yield `[${s}]`; +}); +chained.on("data", (s) => log.push(`chained ${s}`)); +pass.write("x"); +pass.end("yz"); +await new Promise((resolve) => chained.on("end", resolve)); + +const ac = new AbortController(); +const signal = ac.signal; +const slow = new Readable({ objectMode: true, read() {} }); +const aborted = slow.compose(lengths, { signal }); +aborted.on("data", (s) => log.push(`aborted data ${s}`)); +aborted.on("error", (e) => log.push(`aborted error ${e.name} ${e.code}`)); +aborted.on("close", () => log.push(`aborted close ${aborted.destroyed}`)); +slow.push("one"); +setTimeout(() => ac.abort(), 10); +await new Promise((resolve) => aborted.on("close", resolve)); + +const source3 = Readable.from(WORDS); +const unsignaled = source3.compose(lengths, undefined); +log.push(`undefined options ${(await unsignaled.toArray()).length}`); + +console.log(log.join("\n")); diff --git a/tests/corpus/7257-pipeline-generator-destination.mjs b/tests/corpus/7257-pipeline-generator-destination.mjs new file mode 100644 index 000000000..0996e06eb --- /dev/null +++ b/tests/corpus/7257-pipeline-generator-destination.mjs @@ -0,0 +1,61 @@ +// pipeline() whose destination is a function: an async generator +// destination is pumped into the object-mode PassThrough pipeline() +// returns, and an async function destination's resolved value is written +// to it (null and undefined are not). The callback runs with the error +// and the destination's value; a generator yielding null fails the +// pipeline with ERR_STREAM_NULL_VALUES. +import { Readable, pipeline } from "node:stream"; + +const log = []; + +const out = pipeline( + Readable.from(["a", "b", "c"]), + async function* (source) { + for await (const chunk of source) yield { chunk, at: log.length }; + }, + (err, value) => log.push(`gen cb ${err} ${value}`), +); +log.push(`gen out r=${out.readable} w=${out.writable} ro=${out.readableObjectMode} wo=${out.writableObjectMode}`); +for await (const item of out) log.push(`gen read ${JSON.stringify(item)}`); + +const summary = pipeline( + Readable.from([3, 4]), + async function* (source) { + for await (const n of source) yield n * n; + }, + async function (source) { + let total = 0; + for await (const n of source) total += n; + return { total }; + }, + (err, value) => log.push(`fn cb ${err} ${JSON.stringify(value)}`), +); +summary.on("data", (v) => log.push(`fn data ${JSON.stringify(v)}`)); +summary.on("end", () => log.push("fn end")); +await new Promise((resolve) => summary.on("close", resolve)); + +const nothing = pipeline( + Readable.from(["x"]), + async function (source) { + for await (const _ of source); + return undefined; + }, + (err) => log.push(`undefined cb ${err}`), +); +nothing.on("data", () => log.push("undefined data")); +nothing.on("end", () => log.push("undefined end")); +await new Promise((resolve) => nothing.on("close", resolve)); + +const bad = pipeline( + Readable.from(["g"]), + async function* (source) { + for await (const chunk of source) yield chunk; + yield null; + }, + (err) => log.push(`null cb ${err && err.code}`), +); +bad.on("data", (c) => log.push(`null data ${c}`)); +bad.on("error", (e) => log.push(`null error ${e.code}`)); +await new Promise((resolve) => bad.on("close", resolve)); + +console.log(log.join("\n")); diff --git a/tests/corpus/7258-pipeline-generator-destination.ts b/tests/corpus/7258-pipeline-generator-destination.ts new file mode 100644 index 000000000..b22b68dce --- /dev/null +++ b/tests/corpus/7258-pipeline-generator-destination.ts @@ -0,0 +1,47 @@ +// pipeline() with a function destination in typed code. @types/node and +// the fallback declarations type the result as the destination stream; +// for a function destination it is the object-mode PassThrough that +// pipeline() returns, read here as a PassThrough. +import { PassThrough, Readable, pipeline } from "node:stream"; + +const log: string[] = []; + +async function* tokens(source: AsyncIterable): AsyncGenerator { + for await (const line of source) { + for (const word of String(line).split(" ")) yield word; + } +} + +async function* tagged(source: AsyncIterable): AsyncGenerator<{ word: string; n: number }> { + let n = 0; + for await (const word of source) yield { word, n: ++n }; +} + +async function main(): Promise { + const out = pipeline(Readable.from(["to be", "or not"]), tokens, tagged, (err: Error | null) => { + log.push(`callback ${err === null || err === undefined ? "ok" : err.message}`); + }) as PassThrough; + log.push(`object mode ${out.readableObjectMode} ${out.writableObjectMode}`); + for await (const item of out) { + const entry = item as { word: string; n: number }; + log.push(`${entry.n} ${entry.word}`); + } + log.push(`ended ${out.readableEnded}`); + + const counted = pipeline( + Readable.from(["x", "y", "z"]), + async function (source: AsyncIterable): Promise { + let n = 0; + for await (const _ of source) n++; + return n; + }, + (err: Error | null) => log.push(`count callback ${err === null || err === undefined}`), + ) as PassThrough; + const values: unknown[] = []; + counted.on("data", (v: unknown) => values.push(v)); + await new Promise((resolve) => counted.on("end", () => resolve())); + log.push(`count values ${JSON.stringify(values)}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7259-promises-pipeline-stage-options.cjs b/tests/corpus/7259-promises-pipeline-stage-options.cjs new file mode 100644 index 000000000..a4a3ce396 --- /dev/null +++ b/tests/corpus/7259-promises-pipeline-stage-options.cjs @@ -0,0 +1,65 @@ +// stream/promises pipeline() with function stages or an iterable source +// and the options argument: end: false leaves the destination open, a +// signal aborts the pipeline (the stages see their own signal abort), an +// async function destination's value resolves the promise, and a value +// that is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE. +const { PassThrough, Writable } = require("node:stream"); +const { pipeline } = require("node:stream/promises"); + +const log = []; + +const upper = async function* (source) { + for await (const chunk of source) yield String(chunk).toUpperCase(); +}; + +function sink(name) { + return new Writable({ + objectMode: true, + write(chunk, _enc, cb) { + log.push(`${name} got ${chunk}`); + cb(); + }, + }); +} + +async function main() { + const joined = await pipeline(["a", "b"], upper, async function (source) { + let s = ""; + for await (const c of source) s += c; + return s; + }, { end: true }); + log.push(`joined ${joined}`); + + const open = new PassThrough({ objectMode: true }); + await pipeline(["k"], upper, open, { end: false }); + log.push(`end false: ended ${open.writableEnded}`); + open.end("last"); + const rest = []; + for await (const c of open) rest.push(c); + log.push(`end false: read ${rest.join(",")}`); + + const ac = new AbortController(); + const slow = async function* (source, { signal }) { + signal.addEventListener("abort", () => log.push(`stage signal aborted ${signal.aborted}`)); + for await (const chunk of source) { + yield chunk; + await new Promise((resolve) => setTimeout(resolve, 30)); + } + }; + const aborting = pipeline(["s1", "s2", "s3"], slow, sink("abort"), { signal: ac.signal }); + setTimeout(() => ac.abort(), 10); + try { + await aborting; + } catch (e) { + log.push(`aborted ${e.name} ${e.code}`); + } + + try { + await pipeline(["v"], upper, sink("bad"), { signal: "nope" }); + } catch (e) { + log.push(`bad signal ${e.code}`); + } + console.log(log.join("\n")); +} + +main(); diff --git a/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json b/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json new file mode 100644 index 000000000..47f7b9ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7252-duplex-from-generator.ts.json @@ -0,0 +1 @@ +{ "plain": 2522, "san": 3330 } diff --git a/tests/harness/corpus-costs/7253-duplex-from.mjs.json b/tests/harness/corpus-costs/7253-duplex-from.mjs.json new file mode 100644 index 000000000..7743ca7c8 --- /dev/null +++ b/tests/harness/corpus-costs/7253-duplex-from.mjs.json @@ -0,0 +1 @@ +{ "plain": 1849, "san": 2440 } diff --git a/tests/harness/corpus-costs/7254-compose-functions.ts.json b/tests/harness/corpus-costs/7254-compose-functions.ts.json new file mode 100644 index 000000000..19d896024 --- /dev/null +++ b/tests/harness/corpus-costs/7254-compose-functions.ts.json @@ -0,0 +1 @@ +{ "plain": 1782, "san": 2350 } diff --git a/tests/harness/corpus-costs/7255-compose-stages.mjs.json b/tests/harness/corpus-costs/7255-compose-stages.mjs.json new file mode 100644 index 000000000..e8e0ab3d1 --- /dev/null +++ b/tests/harness/corpus-costs/7255-compose-stages.mjs.json @@ -0,0 +1 @@ +{ "plain": 2033, "san": 2680 } diff --git a/tests/harness/corpus-costs/7256-readable-compose.mjs.json b/tests/harness/corpus-costs/7256-readable-compose.mjs.json new file mode 100644 index 000000000..fe1307a1f --- /dev/null +++ b/tests/harness/corpus-costs/7256-readable-compose.mjs.json @@ -0,0 +1 @@ +{ "plain": 2146, "san": 2830 } diff --git a/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json b/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json new file mode 100644 index 000000000..ebaa9214c --- /dev/null +++ b/tests/harness/corpus-costs/7257-pipeline-generator-destination.mjs.json @@ -0,0 +1 @@ +{ "plain": 2008, "san": 2731 } diff --git a/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json b/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json new file mode 100644 index 000000000..15ff27f21 --- /dev/null +++ b/tests/harness/corpus-costs/7258-pipeline-generator-destination.ts.json @@ -0,0 +1 @@ +{ "plain": 1751, "san": 2442 } diff --git a/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json b/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json new file mode 100644 index 000000000..6bda6d818 --- /dev/null +++ b/tests/harness/corpus-costs/7259-promises-pipeline-stage-options.cjs.json @@ -0,0 +1 @@ +{ "plain": 2504, "san": 3707 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c0176eb93..c4013d288 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 5ceec5659c799a280e2f47a4a52920a86c903007 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:06:14 -0500 Subject: [PATCH 029/169] Keep key orders exact through write values, live views and spreads Three cases where a value's key order could differ from Node: - A write of an optional key moved the key before its value was evaluated. A value that adds another key to the same record, as in `o.b = ((o.c = 1), 2)`, then put the keys in the wrong order. The write now evaluates its value first, then tests and moves the key. - A value seen as a JavaScript value bound the field hooks of its order when it was boxed, and a later write that moved a key did not change them. Shapes whose values can change order after they are built (IrRecordShape.orderMoves) now read through the value's view, which follows the current order. - A literal that spreads a value with orders and then sets a key the value may lack, as in `{ ...value, tag: "x" }`, put that key where the source's order has it. Node puts it there only when the value has it, and after the value's keys otherwise. The literal now picks its order from the source's order and which of those keys the source has. Corpus 7136 and 7138 cover each case. --- .../src/backend/llvm/expr-stream-bridges.ts | 10 +- .../lowering/key-order-witness.test.ts | 108 ++++++++++++++ .../frontend/lowering/key-order-witness.ts | 138 +++++++++++++----- packages/compiler/src/ir/ir.ts | 4 + tests/corpus/7136-key-order-copies.ts | 13 ++ tests/corpus/7138-key-order-late-writes.ts | 14 +- .../7136-key-order-copies.ts.json | 2 +- .../7138-key-order-late-writes.ts.json | 2 +- 8 files changed, 251 insertions(+), 40 deletions(-) diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index b64204122..398384e75 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -1071,9 +1071,15 @@ export function streamTypedRefMaterializeAdapter( }; // Published before the field converters, like the array hooks below. // A Dirent row reads through its finished view (the prototype and - // the non-enumerable %dtype). + // the non-enumerable %dtype). A capsule binds the ops of its value's + // order when it is built, so a shape whose values can change order + // later (orderMoves) reads through its view. const dirent = isDirentShape(shape); - if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) { + if ( + !dirent && + shape.orderMoves === undefined && + orders.every((order) => liveRecordOpsEligible(fieldsOf(order))) + ) { if (orders.length === 1) { adapter.recordOps = `@${snapshot}_rops`; liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!)); 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 56b65a3d3..3b64d65ce 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -468,3 +468,111 @@ test("re-adding a key a literal creates earlier moves it to the end of that valu ["b", "a"], ]); }); + +test("a moving write evaluates its value before it tests and moves the key", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + // `o.x = ((o.y = 2), 3)`: y is created first. + const value: IrExpr = { + kind: "seqExpr", + stmts: [set(ref("o", o), "y")], + result: num(3), + type: F64, + loc, + }; + const write: IrStmt = { + kind: "recordSet", + obj: ref("o", o), + shapeId: o.shapeId, + field: "x", + value, + loc, + }; + const fn = f.run([ + decl("o", f.literal(o, ["id"])), + observe(f.literal(o, ["x", "id"])), + write, + observe(ref("o", o)), + ]); + const shape = f.shapes.get(o.shapeId)!; + expect(shape.orderVariants).toBeDefined(); + expect(shape.orderMoves).toBe(true); + const block = fn.body[2] as IrStmt & { kind: "block" }; + expect(block.kind).toBe("block"); + const [first, guard, written] = block.body; + expect(first).toMatchObject({ kind: "varDecl", init: { kind: "seqExpr" } }); + const local = (first as IrStmt & { kind: "varDecl" }).localId; + expect(guard).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); + expect(written).toMatchObject({ + kind: "recordSet", + field: "x", + value: { kind: "varRef", localId: local }, + }); + // The write inside the value moves y in its own block. + const init = (first as IrStmt & { kind: "varDecl" }).init as IrExpr & { kind: "seqExpr" }; + expect(init.stmts[0]).toMatchObject({ kind: "block" }); +}); + +test("a key after a spread that the source may lack picks its order from the source's keys", () => { + const f = fixture(); + const s = f.record( + [ + ["a", F64], + ["b", OPT], + ["c", OPT], + ], + ["a", "b", "c"], + ); + const t = f.record( + [ + ["a", F64], + ["b", F64], + ["c", OPT], + ], + ["a", "b", "c"], + ); + const read = (field: string): IrExpr => ({ + kind: "recordGet", + obj: ref("x", s), + shapeId: s.shapeId, + field, + type: OPT, + loc, + }); + // `{ ...x, b: 1 }` + const merged = f.notes.literal( + { + kind: "recordLit", + fields: [ + { name: "a", value: read("a") }, + { name: "c", value: read("c") }, + { name: "b", value: num(1) }, + ], + type: t, + loc, + }, + [ + { kind: "spread", shapeId: s.shapeId, maybeAbsent: false }, + { kind: "key", name: "b", definite: true }, + ], + ); + f.run([decl("x", f.literal(s, ["c", "a"])), observe(f.literal(s, ["a", "c"])), observe(merged)]); + // b keeps its place in the source's order when the source has it, and + // follows the source's keys when it does not. + expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([ + ["a", "c", "b"], + ["c", "a", "b"], + ["a", "b", "c"], + ]); + expect(orderIndexOf(merged)).toMatchObject({ + kind: "ternary", + cond: { kind: "recordHas", obj: { kind: "varRef", localId: "x" }, field: "b" }, + }); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index c35860d43..4eef5dc54 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -768,7 +768,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // table, as before. // A write of an optional key a value lacks moves the key to the end of // the value's order, as in Node: the orders include every order such - // writes reach, and each write site updates the index first. + // writes reach, and each write site updates the index after it evaluates + // the value and before it writes the key. const MAX_ORDERS = 16; const unresolved = (id: string): boolean => result.get(id) === "unknown"; /** `order` over every key of `table`: keys the order lacks follow it. */ @@ -809,6 +810,30 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio } return [...moving]; }; + /** `values[i]` for the order index `current` holds, as a chain of + * tests. */ + const indexChain = (current: IrExpr, values: readonly number[], loc: SrcLoc): IrExpr => { + let out: IrExpr = { kind: "numLit", value: values[values.length - 1]!, type: F64, loc }; + for (let i = values.length - 2; i >= 0; i--) { + if (out.kind === "numLit" && out.value === values[i]) continue; + out = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: current, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: values[i]!, type: F64, loc }, + else_: out, + type: F64, + loc, + }; + } + return out; + }; const moves = new Map>(); /** Adds to `orders` every order writes of `moving` keys reach from them * and records each key's transitions; false past MAX_ORDERS. */ @@ -887,8 +912,17 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // way: copies (rest bindings, width copies), or literals spreading the // value between the same explicit keys. Order i is the source's order i // over the shape's keys, after the keys before the spread and before - // the keys after it. - type Template = { source: string; prefix: string[]; suffix: string[] }; + // the keys after it. A key after the spread that the source value may + // lack goes where the source has it, or after the source's keys when it + // has not: such a literal's orders are one per source order and set of + // those keys the source has, and `select.index[present][i]` is the order + // for source order i with the keys of bit set `present`. + type Template = { + source: string; + prefix: string[]; + suffix: string[]; + select?: { keys: string[]; index: number[][] }; + }; const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => { const prefix: string[] = []; const suffix: string[] = []; @@ -924,11 +958,39 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const orders = variants.get(first.source); if (orders === undefined || mirrors.has(first.source)) continue; const table = tableOf(candidate.shape); - const mirrored = orders.map((o) => { + const tested = first.suffix.filter((k) => maybeAbsent(first.source, k)); + if (tested.length > 4) continue; + const mirrorOf = (o: readonly string[], lacks: ReadonlySet): string[] => { const order = [...first.prefix]; - for (const k of [...o, ...first.suffix]) if (!order.includes(k)) order.push(k); + for (const k of o) if (!lacks.has(k) && !order.includes(k)) order.push(k); + for (const k of first.suffix) if (!order.includes(k)) order.push(k); return complete(order, table); - }); + }; + let mirrored: string[][]; + let template: Template = first; + if (tested.length === 0) mirrored = orders.map((o) => mirrorOf(o, new Set())); + else { + mirrored = []; + const index: number[][] = []; + for (let present = 0; present < 1 << tested.length; present++) { + const lacks = new Set(tested.filter((_, j) => (present & (1 << j)) === 0)); + const row: number[] = []; + for (const o of orders) { + const order = mirrorOf(o, lacks); + let i = indexOfOrder(mirrored, order); + if (i < 0) i = mirrored.push(order) - 1; + row.push(i); + } + index.push(row); + } + if (mirrored.length > MAX_ORDERS) continue; + template = { + source: first.source, + prefix: first.prefix, + suffix: first.suffix, + select: { keys: tested, index }, + }; + } if (!closeUnderMoves(id, mirrored, candidate.moving)) continue; if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) { // Every order of the source agrees over these keys: one table. @@ -940,7 +1002,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio continue; } variants.set(id, mirrored); - mirrors.set(id, first); + mirrors.set(id, template); } if (variants.size === 0) return; @@ -995,7 +1057,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio if (segments !== undefined && mirror !== undefined) { const obj = copySource(lit, mirror.source); if (obj === null) return null; - return { + const sourceIndex: IrExpr = { kind: "recordGet", obj, shapeId: mirror.source, @@ -1003,6 +1065,22 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio type: F64, loc: lit.loc, }; + const select = mirror.select; + if (select === undefined) return sourceIndex; + // Tests the source for each key in turn, then picks by its index. + const pick = (j: number, present: number): IrExpr => { + if (j === select.keys.length) + return indexChain(sourceIndex, select.index[present]!, lit.loc); + return { + kind: "ternary", + cond: lowerer.recordFieldPresent(obj, mirror.source, select.keys[j]!, lit.loc), + then: pick(j + 1, present | (1 << j)), + else_: pick(j + 1, present), + type: F64, + loc: lit.loc, + }; + }; + return pick(0, 0); } if (segments !== undefined) { const order = literalOrder(segments); @@ -1072,7 +1150,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index; lit.fields.push({ name: KEY_ORDER_FIELD, value }); } - let receivers = 0; + let temporaries = 0; const replacements = new Map>(); for (const { st, fn } of writeSites) { if (dropped.has(st.shapeId)) continue; @@ -1084,7 +1162,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const prologue: IrStmt[] = []; let obj = st.obj; if (plain.kind !== "varRef") { - const localId = `%keyOrderReceiver.${receivers++}`; + const localId = `%keyOrderReceiver.${temporaries++}`; fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false }); prologue.push({ kind: "varDecl", localId, init: st.obj, loc }); obj = { kind: "varRef", localId, type: st.obj.type, loc }; @@ -1097,29 +1175,20 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio type: F64, loc, }; - // transitions[i] for the current index i, as a chain of tests. - let next: IrExpr = { - kind: "numLit", - value: transitions[transitions.length - 1]!, - type: F64, - loc, - }; - for (let i = transitions.length - 2; i >= 0; i--) { - next = { - kind: "ternary", - cond: { - kind: "bin", - op: "===", - left: current, - right: { kind: "numLit", value: i, type: F64, loc }, - type: BOOL, - loc, - }, - then: { kind: "numLit", value: transitions[i]!, type: F64, loc }, - else_: next, - type: F64, - loc, - }; + const next = indexChain(current, transitions, loc); + // The value evaluates before the key is tested and moved: it may add + // other keys to the same value, which then come first. + let value = st.value; + const pure = + value.kind === "numLit" || + value.kind === "strLit" || + value.kind === "boolLit" || + value.kind === "varRef"; + if (!pure) { + const localId = `%keyOrderValue.${temporaries++}`; + fn.locals.push({ id: localId, name: "%keyOrderValue", type: value.type, mutable: false }); + prologue.push({ kind: "varDecl", localId, init: value, loc }); + value = { kind: "varRef", localId, type: value.type, loc }; } const absent: IrExpr = { kind: "unary", @@ -1142,7 +1211,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio obj, shapeId: st.shapeId, field: st.field, - value: st.value, + value, loc, }; const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc }; @@ -1172,5 +1241,6 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio tables.push(table); } shape.orderVariants = tables; + if (moves.has(id)) shape.orderMoves = true; } } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 89a5ee73a..a9f5dda24 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1673,6 +1673,10 @@ export interface IrRecordShape { * and every other construction leaves 0. Each key-order surface * dispatches on that field. */ orderVariants?: string[][]; + /** A shape with orderVariants whose values can change order after they + * are built: a write of an optional key a value lacks updates its + * KEY_ORDER_FIELD. */ + orderMoves?: true; } /** The hidden field of a shape with orderVariants: the index of the value's diff --git a/tests/corpus/7136-key-order-copies.ts b/tests/corpus/7136-key-order-copies.ts index daafba23b..5d754bced 100644 --- a/tests/corpus/7136-key-order-copies.ts +++ b/tests/corpus/7136-key-order-copies.ts @@ -17,3 +17,16 @@ for (const value of [a, b, c]) { console.log(name, JSON.stringify(rest), Object.keys(rest).join(",")); console.log(JSON.stringify(assigned), JSON.stringify(cloned)); } +// Keys after a spread that the spread value may lack: a key the value has +// keeps its place, and a key it lacks goes after the value's keys. +type Options = { id: number; label?: string; size?: number; tag?: string }; +const options: Options[] = [ + { id: 1, size: 2 }, + { size: 3, id: 2 }, + { label: "c", id: 3, tag: "t" }, + { tag: "u", size: 4, id: 4 }, +]; +for (const value of options) { + const merged = { ...value, tag: "x", label: "y", done: true }; + console.log(Object.keys(merged).join(","), JSON.stringify(merged)); +} diff --git a/tests/corpus/7138-key-order-late-writes.ts b/tests/corpus/7138-key-order-late-writes.ts index b1afd8761..d0adef3da 100644 --- a/tests/corpus/7138-key-order-late-writes.ts +++ b/tests/corpus/7138-key-order-late-writes.ts @@ -23,5 +23,15 @@ for (let i = 0; i < 2; i++) { console.log(Object.keys(built).join(",")); } console.log(built); -// d -// e +// A write's value runs first, so a key it adds comes before the written +// key. +const nested: Item = { count: 3, id: 4 }; +nested.name = ((nested.note = "first"), "second"); +console.log(Object.keys(nested).join(","), JSON.stringify(nested)); +// A value seen as a JavaScript value enumerates later writes in order. +const seen: Item = { id: 5, count: 4 }; +const view: unknown = seen; +console.log(Object.keys(view as object).join(",")); +seen.note = "n"; +seen.name = "m"; +console.log(Object.keys(view as object).join(","), JSON.stringify(view)); diff --git a/tests/harness/corpus-costs/7136-key-order-copies.ts.json b/tests/harness/corpus-costs/7136-key-order-copies.ts.json index ba3e270ee..9d76a548c 100644 --- a/tests/harness/corpus-costs/7136-key-order-copies.ts.json +++ b/tests/harness/corpus-costs/7136-key-order-copies.ts.json @@ -1 +1 @@ -{ "plain": 1700, "san": 2400 } +{ "plain": 1900, "san": 3500 } diff --git a/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json index 0b945a1c9..2b1aad28d 100644 --- a/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json +++ b/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json @@ -1 +1 @@ -{ "plain": 1700, "san": 2500 } +{ "plain": 1700, "san": 2800 } From 291587aae17242e55cb6a1ab9801735cb22d4e49 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:08:55 -0500 Subject: [PATCH 030/169] Scan regular expressions after jump statements, and give up on non-ASCII whitespace --- .../src/frontend/token-equivalence.test.ts | 22 +++++++++++ .../src/frontend/token-equivalence.ts | 37 ++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts index afd81d9b6..18bfb32e6 100644 --- a/packages/compiler/src/frontend/token-equivalence.test.ts +++ b/packages/compiler/src/frontend/token-equivalence.test.ts @@ -37,6 +37,28 @@ test("comment-like text inside regular expressions is code", () => { ).toBe(false); }); +test("a regular expression can start after a statement that ends at its keyword or label", () => { + const loop = (body: string) => `a: for (;;) {\n${body}\n}\n`; + expect( + equivalent(loop("break\n/[//]/.test(x); c = 1;"), loop("break\n/[//]/.test(x); c = 2;")), + ).toBe(false); + expect( + equivalent(loop("break a\n/[//]/.test(x); c = 1;"), loop("break a\n/[//]/.test(x); c = 2;")), + ).toBe(false); + expect( + equivalent("debugger\n/[//]/.test(x); c = 1;\n", "debugger\n/[//]/.test(x); c = 2;\n"), + ).toBe(false); + // The next line's `x` is no label: it starts a new statement, and `/` divides. + expect(equivalent(loop("continue\nx / 2; // one"), loop("continue\nx / 2; // two"))).toBe(true); +}); + +test("whitespace outside ASCII is never equivalent", () => { + expect( + equivalent("return\u3000/[//]/.test(x); c = 1;\n", "return\u3000/[//]/.test(x); c = 2;\n"), + ).toBe(false); + expect(equivalent("const a = 1;\u00a0// one\n", "const a = 1;\u00a0// two\n")).toBe(false); +}); + test("U+2028 and U+2029 end a line comment, so their files are never equivalent", () => { expect(equivalent("// a\u2028c = 1;\n", "// a\u2028c = 2;\n")).toBe(false); expect(equivalent("/* a\u2029*/ c = 1;\n", "/* b\u2029*/ c = 1;\n")).toBe(false); diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts index 94c60d836..b13b67498 100644 --- a/packages/compiler/src/frontend/token-equivalence.ts +++ b/packages/compiler/src/frontend/token-equivalence.ts @@ -19,10 +19,13 @@ * cannot tell which, so it gives up, as it does for escaped identifiers, * which can spell keywords. * - Template literals track their `${` substitutions on a brace stack. - * - JSX files, files that contain U+2028 or U+2029 anywhere (they end a - * line comment and separate lines for automatic semicolon insertion), - * and unterminated strings, comments, templates and regular expressions - * give up. + * - After `break`, `continue` and `debugger`, and after the label of a + * `break` or `continue`, a `/` starts a regular expression: it can only + * follow there once automatic semicolon insertion ends the statement. + * - JSX files, files that contain whitespace or a line terminator outside + * ASCII anywhere (the identifier scan would take it as part of a word, and + * U+2028 and U+2029 end a line comment), and unterminated strings, + * comments, templates and regular expressions give up. * * Plain character scans keep the scanner free of Unicode property escapes: * any non-ASCII code unit outside strings, templates, regular expressions @@ -55,6 +58,12 @@ const EXPRESSION_KEYWORDS = new Set([ "extends", ]); +/** Statements that end at their keyword, or at the label after it. */ +const JUMP_KEYWORDS = new Set(["break", "continue", "debugger"]); + +/** Whitespace and line terminators outside ASCII. */ +const NON_ASCII_SPACE = /[\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/; + /** Keywords whose parenthesized header is followed by a statement. */ const HEADER_KEYWORDS = new Set(["if", "while", "for", "with"]); @@ -120,7 +129,7 @@ function isSpace(code: number): boolean { /** The tokens of `source`, or null when the scanner cannot classify it. */ function scan(path: string, source: string): Token[] | null { if (path.endsWith(".tsx") || path.endsWith(".jsx")) return null; - if (source.includes("\u2028") || source.includes("\u2029")) return null; + if (NON_ASCII_SPACE.test(source)) return null; const typeScript = isTypeScript(path); const tokens: Token[] = []; // Brace kinds: true for a template substitution, false otherwise. @@ -128,14 +137,23 @@ function scan(path: string, source: string): Token[] | null { // Paren kinds: true for a statement header. const parens: boolean[] = []; let lineBreakBefore = false; - // The previous token and the one before it, and whether a `)` closed a - // statement header. - const last: { token: string | null; before: string | null } = { token: null, before: null }; + // The previous token and the one before it, whether the previous token is + // the label of a `break` or `continue` on its line, and whether a `)` + // closed a statement header. + const last: { token: string | null; before: string | null; label: boolean } = { + token: null, + before: null, + label: false, + }; let closedHeader = false; const push = (text: string, comment = false): void => { tokens.push({ text, comment, lineBreakBefore }); lineBreakBefore = false; if (!comment) { + last.label = + (last.token === "break" || last.token === "continue") && + !tokens[tokens.length - 1]!.lineBreakBefore && + /^[A-Za-z_$]/.test(text); last.before = last.token; last.token = text; } @@ -184,7 +202,6 @@ function scan(path: string, source: string): Token[] | null { index++; continue; } - if (code > 127 && (code === 0xa0 || code === 0xfeff)) return null; const next = source.charCodeAt(index + 1); // Comments. if (code === 47 && (next === 47 || next === 42)) { @@ -276,7 +293,7 @@ function scan(path: string, source: string): Token[] | null { else if (before === "++" || before === "--") return null; else if (before === "]") regex = false; else if (/^[A-Za-z0-9_$\\]/.test(before) || before.charCodeAt(0) > 127) - regex = EXPRESSION_KEYWORDS.has(before); + regex = EXPRESSION_KEYWORDS.has(before) || JUMP_KEYWORDS.has(before) || last.label; else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`")) regex = false; else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false; From 2b657e6e3cca7bb12f60ef1cb199ce79e73ac198 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:12:02 -0500 Subject: [PATCH 031/169] Format the split tools' output with the tools checkout's oxfmt configuration The split tools ran oxfmt in the checkout they rewrite. oxfmt looks up its configuration from that directory and from the formatted files' directories, and runs an oxfmt.config.ts it finds there, so a branch that rehome-split rewrites could run code with the maintainer's permissions. The tools now format through format.mjs, which passes the tools checkout's .oxfmtrc.jsonc with --config and disables the nested lookup, so no configuration file of the rewritten checkout is read or run. With no files to format it does nothing, where oxfmt would format the whole directory. The split-tools tests cover split-methods and move-state on a checkout whose root and src directory commit an oxfmt.config.ts that records it ran. --- scripts/split/README.md | 4 ++- scripts/split/format.mjs | 20 +++++++++++ scripts/split/move-state.mjs | 5 ++- scripts/split/rehome-split.mjs | 6 ++-- scripts/split/split-methods.mjs | 7 ++-- scripts/split/split-module.mjs | 7 ++-- tests/harness/split-tools.test.ts | 57 +++++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 scripts/split/format.mjs diff --git a/scripts/split/README.md b/scripts/split/README.md index e3cdfd077..be8c948ac 100644 --- a/scripts/split/README.md +++ b/scripts/split/README.md @@ -5,13 +5,15 @@ These scripts split a large source file into smaller files without changing beha Run them from a checkout with `node_modules` installed. That checkout is the tools directory (`--tools`) and supplies `typescript` and `oxfmt`. ## Transforms + - `split-module.mjs plan|apply ` moves top-level declarations into new files. `plan` computes a manifest from seed declarations, and `apply` does the move. - `split-methods.mjs apply ` moves class methods into exported functions that take the instance as a parameter (`emitter`). The class keeps a one-line stub with the same signature, so callers don't change. - `move-state.mjs apply ` moves a group of class fields into a feature's state object, defined in the feature's file with `defineModuleState` (`packages/compiler/src/backend/llvm/emitter-state.ts`). It rewrites `this.` and `emitter.` to `KEY.of(this).` and creates files from templates. It also applies the change's other edits as literal replacements, in order. A replacement whose text isn't found the expected number of times is reported and skipped. -Each transform takes `--repo

` (the tree to rewrite) and `--tools `, and formats what it writes with oxfmt. The tools read and write the tree's files only at real paths inside `--repo`, and a config's templates and manifest only inside the config's directory. A path that resolves elsewhere, through a symlink or `..`, stops the tool with an error (`confine.mjs`). `configs/` holds the configs of splits that have landed. For example, `configs/loop-state/` moves the loop optimizations' state out of `LlEmitter`. +Each transform takes `--repo ` (the tree to rewrite) and `--tools `, and formats what it writes with the tools checkout's oxfmt and its `.oxfmtrc.jsonc` (`format.mjs`). The formatter reads no configuration from `--repo`, so an `oxfmt.config.ts` a branch commits is never run. The tools read and write the tree's files only at real paths inside `--repo`, and a config's templates and manifest only inside the config's directory. A path that resolves elsewhere, through a symlink or `..`, stops the tool with an error (`confine.mjs`). `configs/` holds the configs of splits that have landed. For example, `configs/loop-state/` moves the loop optimizations' state out of `LlEmitter`. ## Rehoming an open branch + ```sh git fetch origin pull//head:pr- node scripts/split/rehome-split.mjs --branch pr- --onto origin/main --config scripts/split/configs//.json diff --git a/scripts/split/format.mjs b/scripts/split/format.mjs new file mode 100644 index 000000000..10c1fbc8a --- /dev/null +++ b/scripts/split/format.mjs @@ -0,0 +1,20 @@ +/* Formatting for the split tools. oxfmt reads its configuration from the + * directory it runs in, its parents and the formatted files' directories, + * and an oxfmt.config.ts there is code it runs. The tools format files in + * a checkout they rewrite, which can be a pull request's, so they pass the + * tools checkout's own configuration and turn the nested lookup off: no + * configuration file of the rewritten checkout is read or run. */ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; + +/** Formats `files` (paths relative to `cwd`) with the tools checkout's + * oxfmt and configuration. */ +export function formatFiles(toolsDir, files, { cwd, stdio }) { + // With no file arguments oxfmt would format the whole directory. + if (files.length === 0) return; + execFileSync( + join(toolsDir, "node_modules/.bin/oxfmt"), + ["--config", join(toolsDir, ".oxfmtrc.jsonc"), "--disable-nested-config", ...files], + { cwd, stdio }, + ); +} diff --git a/scripts/split/move-state.mjs b/scripts/split/move-state.mjs index b70023c67..71306f5d1 100644 --- a/scripts/split/move-state.mjs +++ b/scripts/split/move-state.mjs @@ -26,8 +26,8 @@ import { readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { createRequire } from "node:module"; -import { execFileSync } from "node:child_process"; import { confine } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; const [command, configPath, ...flags] = process.argv.slice(2); if (command !== "apply") throw new Error("usage: move-state.mjs apply "); @@ -199,8 +199,7 @@ for (const r of config.replacements ?? []) { checkout.write(path, text.split(r.find).join(r.replace)); touched.add(path); } -const oxfmt = join(toolsDir, "node_modules/.bin/oxfmt"); -execFileSync(oxfmt, [...touched], { cwd: repo, stdio: "inherit" }); +formatFiles(toolsDir, [...touched], { cwd: repo, stdio: "inherit" }); if (skipped) console.warn(`${skipped} replacement(s) skipped`); console.log( `moved ${config.groups.map((g) => g.present.length).reduce((a, b) => a + b, 0)} fields; rewrote ${touched.size} files`, diff --git a/scripts/split/rehome-split.mjs b/scripts/split/rehome-split.mjs index 89c32d6b7..6533301f8 100644 --- a/scripts/split/rehome-split.mjs +++ b/scripts/split/rehome-split.mjs @@ -29,6 +29,7 @@ import { createRequire } from "node:module"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { confine } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; const args = process.argv.slice(2); const opt = (name, fallback) => { @@ -173,10 +174,7 @@ try { splitFiles.has(file) && resolveImportConflicts(confine(resultDir), join(resultDir, file)) ) { - execFileSync(join(tools, "node_modules/.bin/oxfmt"), [file], { - cwd: resultDir, - stdio: ["ignore", "ignore", "inherit"], - }); + formatFiles(tools, [file], { cwd: resultDir, stdio: ["ignore", "ignore", "inherit"] }); gitIn(resultDir, "add", file); console.log(`${file}: import conflicts resolved to both sides' imports`); } diff --git a/scripts/split/split-methods.mjs b/scripts/split/split-methods.mjs index 05d72c4be..4d57644af 100644 --- a/scripts/split/split-methods.mjs +++ b/scripts/split/split-methods.mjs @@ -23,8 +23,8 @@ import { readFileSync } from "node:fs"; import { dirname, join, relative, resolve, posix } from "node:path"; import { createRequire } from "node:module"; -import { execFileSync } from "node:child_process"; import { confine, confineHost } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; const [command, configPath, ...flags] = process.argv.slice(2); if (command !== "apply") throw new Error("usage: split-methods.mjs apply "); @@ -413,8 +413,5 @@ const touched = [ modulePath, ...[...files].filter(([, e]) => e.decls.length).map(([f]) => join(moduleDir, f)), ]; -execFileSync(join(toolsDir, "node_modules/.bin/oxfmt"), touched, { - cwd: repo, - stdio: ["ignore", "ignore", "inherit"], -}); +formatFiles(toolsDir, touched, { cwd: repo, stdio: ["ignore", "ignore", "inherit"] }); console.log(`${moved} methods moved; ${publicize.size} members reached through host`); diff --git a/scripts/split/split-module.mjs b/scripts/split/split-module.mjs index ba340df40..4b8764d80 100644 --- a/scripts/split/split-module.mjs +++ b/scripts/split/split-module.mjs @@ -39,8 +39,8 @@ import { readFileSync } from "node:fs"; import { dirname, join, relative, resolve, posix } from "node:path"; import { createRequire } from "node:module"; -import { execFileSync } from "node:child_process"; import { confine, confineHost } from "./confine.mjs"; +import { formatFiles } from "./format.mjs"; const [command, configPath, ...flags] = process.argv.slice(2); const config = JSON.parse(readFileSync(configPath, "utf8")); @@ -533,10 +533,7 @@ function apply() { // Format what changed with the tools checkout's oxfmt, so the split and // every rehome of it are formatted the same way. const touched = [modulePath, ...[...files.keys()].map((f) => join(moduleDir, f))]; - execFileSync(join(toolsDir, "node_modules/.bin/oxfmt"), touched, { - cwd: repo, - stdio: ["ignore", "ignore", "inherit"], - }); + formatFiles(toolsDir, touched, { cwd: repo, stdio: ["ignore", "ignore", "inherit"] }); for (const [file, entry] of files) console.log(`${file}: ${entry.decls.length} declarations`); } diff --git a/tests/harness/split-tools.test.ts b/tests/harness/split-tools.test.ts index 08d89e850..8bdefdf0e 100644 --- a/tests/harness/split-tools.test.ts +++ b/tests/harness/split-tools.test.ts @@ -97,6 +97,63 @@ test("split-methods keeps an import a staying method uses as a shorthand propert expect(diagnostics(dir)).toEqual([]); }); +/** An oxfmt.config.ts that records it ran by writing `marker`. */ +const recordingConfig = (marker: string): string => + [ + 'import { writeFileSync } from "node:fs";', + `writeFileSync(${JSON.stringify(marker)}, "ran");`, + "export default {};", + "", + ].join("\n"); + +test("split-methods formats with the tools checkout's configuration, not the branch's", () => { + const dir = checkout({ + "src/host.ts": [ + "export class Host {", + " value = 1;", + " moved(): number {", + " return this.value + 1;", + " }", + "}", + "", + ].join("\n"), + "split.json": JSON.stringify({ + module: "src/host.ts", + class: "Host", + methods: { "moved.ts": ["moved"] }, + }), + }); + const markers = [join(dir, "root-config-ran"), join(dir, "nested-config-ran")]; + writeFileSync(join(dir, "oxfmt.config.ts"), recordingConfig(markers[0]!)); + writeFileSync(join(dir, "src/oxfmt.config.ts"), recordingConfig(markers[1]!)); + run("split-methods.mjs", ["apply", join(dir, "split.json")], dir); + expect(readFileSync(join(dir, "src/moved.ts"), "utf8")).toContain("export function moved"); + for (const marker of markers) expect(existsSync(marker)).toBe(false); +}); + +test("move-state formats with the tools checkout's configuration, not the branch's", () => { + const dir = checkout({ + "src/host.ts": + "export class Host {\n count = 0;\n moved(): number {\n return this.count + 1;\n }\n}\n", + "state.template": "", + "split.json": JSON.stringify({ + module: "src/host.ts", + class: "Host", + dir: "src", + create: { "host-state.ts": "state.template" }, + groups: [ + { file: "host-state.ts", key: "COUNTS", interfaceName: "Counts", fields: ["count"] }, + ], + }), + }); + const markers = [join(dir, "root-config-ran"), join(dir, "nested-config-ran")]; + writeFileSync(join(dir, "oxfmt.config.ts"), recordingConfig(markers[0]!)); + writeFileSync(join(dir, "src/oxfmt.config.ts"), recordingConfig(markers[1]!)); + run("move-state.mjs", ["apply", join(dir, "split.json")], dir); + expect(readFileSync(join(dir, "src/host.ts"), "utf8")).toContain("COUNTS.of(this).count"); + for (const marker of markers) expect(existsSync(marker)).toBe(false); +}); + test("split-module imports a moved class's local base class as a value", () => { const dir = checkout({ "src/shapes.ts": [ From 7cce36354f20204698523128fc3072b5c58ca5ad Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:21:10 -0500 Subject: [PATCH 032/169] Dispatch checked-dynamic method calls on method ids A method call on a value of unknown type (untyped JavaScript, `any`) picked its builtin by comparing the method name with strcmp against each candidate, in long ladders per receiver kind: a string method could test dozens of names before it matched. - The name is now looked up once per call in a generated hash table (scr_dyn_methods.h, from scripts/gen-dyn-methods.mjs), and each ladder compares the resulting id. The table verifies the name's bytes, so a name is matched by its content, as before. - The worker event methods compare the first character before strcmp. - bench/dynamic-methods calls string, array, Map, Set, number and own methods from untyped JavaScript. --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/dynamic-methods.json | 6 + .../runtime/workloads/dynamic-methods.mjs | 36 +++ packages/runtime/src/scr_dyn_invoke.c | 255 +++++++++--------- packages/runtime/src/scr_dyn_methods.h | 160 +++++++++++ packages/runtime/src/scr_dyn_methods.test.ts | 10 + packages/runtime/src/scr_worker_events.c | 40 +-- scripts/gen-dyn-methods.mjs | 101 +++++++ scripts/zero-cost-compare.mjs | 4 +- 9 files changed, 473 insertions(+), 140 deletions(-) create mode 100644 benchmarks/runtime/workloads/dynamic-methods.json create mode 100644 benchmarks/runtime/workloads/dynamic-methods.mjs create mode 100644 packages/runtime/src/scr_dyn_methods.h create mode 100644 packages/runtime/src/scr_dyn_methods.test.ts create mode 100644 scripts/gen-dyn-methods.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 2653ec51a..e8acbfbb5 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -11,6 +11,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any | | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `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 | +| `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 | | `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-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json new file mode 100644 index 000000000..fa27f4ea8 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-methods.mjs", + "args": [ + "300000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs new file mode 100644 index 000000000..bbf217701 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.mjs @@ -0,0 +1,36 @@ +// Untyped JavaScript calling builtin methods on values it never declares +// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array +// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number +// toFixed, and calls of an object's own methods. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "300000"); +const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]; +const counts = new Map(); +const seen = new Set(); +const stack = []; +const recent = []; +const tally = { + total: 0, + add(value) { + this.total += value; + return this.total; + }, +}; +let checksum = 0; +for (let i = 0; i < rounds; i++) { + const word = words[i % words.length]; + const head = word.slice(0, 3).toUpperCase(); + const key = head.padStart(4, "-"); + if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1; + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 97); + if (seen.has(i % 89)) checksum += 2; + stack.push(i); + if (stack.length > 32) checksum += stack.pop() % 3; + recent.push(word); + if (recent.length > 4) recent.shift(); + if (recent.includes("gamma")) checksum += recent.indexOf("gamma"); + checksum += tally.add(i % 5) % 11; + if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0; +} +console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 80e01b08c..b0f30ac74 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -8,6 +8,7 @@ * scr_json.c; this unit uses only its public surface plus ScrJsonBuf. */ #include "scr_runtime.h" +#include "scr_dyn_methods.h" #include #include @@ -549,7 +550,11 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return scr_dyn_retain(scr_dyn_undefined()); } -static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; } +/* The dispatch below maps a call's method name to an id once + * (dyn_method_of, scr_dyn_methods.h) and its branches compare ids. Names + * compare by content, and any name the branches do not test is DYN_M_NONE. + * scripts/gen-dyn-methods.mjs writes the header from the DYN_M_ ids used + * here. */ /* Handle receivers: hand the whole call to the tag's ops (installed by * the owning unit at main() — a missing install is an internal error the @@ -692,25 +697,30 @@ static bool dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) { /* Names each prototype declares BEYOND what's implemented here — these * throw the "not supported yet" Error instead of mis-answering "is not a * function". */ -static bool dyn_arr_proto_unimpl(const char *m) { - static const char *names[] = { "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_unimpl(int m) { + return m == DYN_M_toString || m == DYN_M_toLocaleString; } -static bool dyn_arr_proto_mutates(const char *m) { - static const char *names[] = { - "push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL - }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_mutates(int m) { + switch (m) { + case DYN_M_push: case DYN_M_pop: case DYN_M_shift: case DYN_M_unshift: case DYN_M_reverse: + case DYN_M_sort: case DYN_M_fill: case DYN_M_copyWithin: case DYN_M_splice: + return true; + default: + return false; + } } -static bool dyn_bytes_proto_real(const char *m) { - static const char *names[] = { "slice", "at", "indexOf", "lastIndexOf", "includes", "join", - "forEach", "map", "filter", "some", "every", "find", "findIndex", "reverse", "fill", "set", - "subarray", "sort", "keys", "values", "entries", "reduce", "reduceRight", "copyWithin", - "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_bytes_proto_real(int m) { + switch (m) { + case DYN_M_slice: case DYN_M_at: case DYN_M_indexOf: case DYN_M_lastIndexOf: case DYN_M_includes: + case DYN_M_join: case DYN_M_forEach: case DYN_M_map: case DYN_M_filter: case DYN_M_some: + case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: + case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: + case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: + case DYN_M_toString: case DYN_M_toLocaleString: + return true; + default: + return false; + } } static ScrDyn *scr_dyn_invoke_impl( @@ -724,18 +734,18 @@ static ScrDyn *scr_dyn_invoke_impl( * array. Anything that could observe more — a hole, argument coercion, a * multi-value push, a value the element type rejects — sets *handled false * and takes the snapshot path. */ -static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args, +static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, const char *what, bool *handled) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(recv); if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; - if (dyn_name_is(method, "push") && argc == 1 && ops->set) { + if ((mid == DYN_M_push) && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) { *handled = true; return scr_dyn_retain(scr_dyn_undefined()); @@ -746,7 +756,7 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *c *handled = true; return last; } - if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { + if ((mid == DYN_M_at) && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { double index = dyn_index_arg(args, argc, 0, 0, what); if (index < 0) index += (double)len; if (index < 0 || index >= (double)len) { @@ -792,6 +802,7 @@ ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, static ScrDyn *scr_dyn_invoke_impl( ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { + const int mid = dyn_method_of(method); if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; scr_jb_init(&b); @@ -811,7 +822,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_HANDLE) { return scr_dynh_dispatch(recv, method, args, argc, what); } - if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(method)) { + if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(mid)) { scr_dyn_array_prototype_element_fence(recv); return NULL; } @@ -836,7 +847,7 @@ static ScrDyn *scr_dyn_invoke_impl( * snapshot identity back to the externally visible capsule. */ if (recv->kind == SCR_DYN_TYPED_REF) { bool handled = false; - ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); + ScrDyn *fast = dyn_live_array_method(recv, mid, args, argc, what, &handled); if (handled) return fast; /* Iterator helpers on a compiled generator. */ if (scr_dyn_generator_helper_hook && @@ -861,7 +872,7 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } bool mutates = materialized->kind == SCR_DYN_ARR && - dyn_arr_proto_mutates(method); + dyn_arr_proto_mutates(mid); ScrDyn *result = scr_dyn_invoke_impl( materialized, method, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); @@ -892,18 +903,18 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_FUNC) { - if (dyn_name_is(method, "apply")) { + if ((mid == DYN_M_apply)) { return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined(), what); } - if (dyn_name_is(method, "call")) { + if ((mid == DYN_M_call)) { scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined()); ScrDyn *r = scr_dyn_call(recv, argc > 1 ? args + 1 : NULL, argc > 1 ? argc - 1 : 0, what); scr_dyn_this_pop(); return r; } - if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc); - if (dyn_name_is(method, "toString")) { + if ((mid == DYN_M_bind)) return scr_dyn_bind(recv, args, argc); + if ((mid == DYN_M_toString)) { ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method)); if (member && member->kind != SCR_DYN_UNDEF) { scr_dyn_this_push_dyn(recv); @@ -938,9 +949,9 @@ static ScrDyn *scr_dyn_invoke_impl( } if ((recv->kind == SCR_DYN_SYMBOL || recv->kind == SCR_DYN_BIGINT || recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) && - dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + (mid == DYN_M_valueOf)) return scr_dyn_retain(recv); - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toFixed")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toFixed)) { double digits = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; ScrStr *text = scr_num_to_fixed(recv->v.num, digits); @@ -949,7 +960,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(text); return result; } - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toExponential")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toExponential)) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { dyn_throw_unsupported("Number", method); return NULL; @@ -962,7 +973,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_STR) { ScrStr *s = recv->v.str; - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double start = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double end = dyn_index_arg(args, argc, 1, scr_str_utf16_len(s), what); @@ -972,16 +983,16 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) && + if ((mid == DYN_M_at) || (mid == DYN_M_concat) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); + double position = dyn_index_arg(args, argc, 1, (mid == DYN_M_lastIndexOf) ? INFINITY : 0, what); if (scr_exc_pending()) return NULL; - if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); - if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + if ((mid == DYN_M_includes)) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if ((mid == DYN_M_indexOf)) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); @@ -992,19 +1003,19 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_ARR) { - if (dyn_name_is(method, "keys") || dyn_name_is(method, "values") || dyn_name_is(method, "entries")) + if ((mid == DYN_M_keys) || (mid == DYN_M_values) || (mid == DYN_M_entries)) return scr_dyn_native_handle_iterator(callback_recv ? callback_recv : recv, - dyn_name_is(method, "keys") ? 1 : dyn_name_is(method, "entries") ? 2 : 0); + (mid == DYN_M_keys) ? 1 : (mid == DYN_M_entries) ? 2 : 0); size_t len = recv->v.arr.len; if (recv->v.arr.presence) { bool sparse = false; for (size_t i = 0; i < len; i++) if (!recv->v.arr.presence[i]) { sparse = true; break; } if (!sparse) { free(recv->v.arr.presence); recv->v.arr.presence = NULL; } - else if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || dyn_name_is(method, "filter") || - dyn_name_is(method, "some") || dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex") || dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex") || - dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight") || dyn_name_is(method, "slice") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { + else if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_reduce) || (mid == DYN_M_reduceRight) || (mid == DYN_M_slice) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes)) { ScrDyn *pack = scr_dyn_new_arr(); for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(pack, scr_dyn_retain(args[i])); ScrStr *name = scr_str_new(method, strlen(method)); @@ -1012,7 +1023,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(name); scr_dyn_release(pack); return result; - } else if (dyn_arr_proto_mutates(method) && !dyn_name_is(method, "fill") && !dyn_name_is(method, "push") && !dyn_name_is(method, "pop") && !dyn_name_is(method, "shift")) { + } else if (dyn_arr_proto_mutates(mid) && !(mid == DYN_M_fill) && !(mid == DYN_M_push) && !(mid == DYN_M_pop) && !(mid == DYN_M_shift)) { static const char message[] = "This checked array mutation over holes is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -1020,11 +1031,11 @@ static ScrDyn *scr_dyn_invoke_impl( } size_t splice_start = 0; size_t splice_removed = 0; - if (dyn_arr_proto_mutates(method)) { - bool grows = dyn_name_is(method, "push") && argc > 0; - grows = grows || (dyn_name_is(method, "unshift") && argc > 0); - bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0; - if (dyn_name_is(method, "splice")) { + if (dyn_arr_proto_mutates(mid)) { + bool grows = (mid == DYN_M_push) && argc > 0; + grows = grows || ((mid == DYN_M_unshift) && argc > 0); + bool shrinks = ((mid == DYN_M_pop) || (mid == DYN_M_shift)) && len > 0; + if ((mid == DYN_M_splice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; splice_start = dyn_rel_index(startD, len); @@ -1061,33 +1072,33 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && (dyn_name_is(method, "push") || - dyn_name_is(method, "pop") || dyn_name_is(method, "shift") || - dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) { + if (recv->v.arr.frozen && ((mid == DYN_M_push) || + (mid == DYN_M_pop) || (mid == DYN_M_shift) || + (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { static const char message[] = "Cannot assign to read only property 'length'"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) || - (recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) || - (dyn_name_is(method, "reverse") && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || + (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || + ((mid == DYN_M_reverse) && len > 1)))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } } - if (dyn_name_is(method, "push")) { + if ((mid == DYN_M_push)) { for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *result = scr_dyn_arr_at(recv, (double)(len - 1)); if (!result) return NULL; scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); return result; } - if (dyn_name_is(method, "shift")) { + if ((mid == DYN_M_shift)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); if (recv->v.arr.sealed) { if (recv->v.arr.frozen) { @@ -1109,7 +1120,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = len - 1; return first; /* ownership moves out */ } - if (dyn_name_is(method, "unshift")) { + if ((mid == DYN_M_unshift)) { /* Grow the backing store before shifting the old elements. The * appended placeholders are immortal, so overwriting them does not * abandon retained argument references. */ @@ -1121,7 +1132,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "splice")) { + if ((mid == DYN_M_splice)) { size_t start = splice_start; size_t removed = splice_removed; size_t inserted = argc > 2 ? argc - 2 : 0; @@ -1151,7 +1162,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = next_len; return out; } - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 1, (double)len, what); @@ -1163,7 +1174,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start; i < end; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i])); return out; } - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1171,17 +1182,17 @@ static ScrDyn *scr_dyn_invoke_impl( if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_arr_at(recv, idx); } - if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + if ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (len == 0) return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); - double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0; + double from = (mid == DYN_M_lastIndexOf) ? (double)len - 1 : 0; if (argc > 1) { from = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; } if (!dyn_arr_length_unchanged(recv, len)) return NULL; - if (dyn_name_is(method, "lastIndexOf")) { + if ((mid == DYN_M_lastIndexOf)) { double first = from < 0 ? (double)len + from : from; if (first >= (double)len) first = (double)len - 1; for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) { @@ -1193,16 +1204,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (first < 0) first = 0; for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) { const ScrDyn *item = recv->v.arr.items[i]; - bool nan_match = dyn_name_is(method, "includes") && + bool nan_match = (mid == DYN_M_includes) && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM && item->v.num != item->v.num && needle->v.num != needle->v.num; if (nan_match || scr_dyn_strict_eq(item, needle)) { - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); } } - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t i = len; i > 0; i--) { @@ -1215,16 +1226,16 @@ static ScrDyn *scr_dyn_invoke_impl( bool matched = scr_dyn_truthy(result); scr_dyn_release(result); if (matched) { - if (dyn_name_is(method, "findLast")) return item; + if ((mid == DYN_M_findLast)) return item; scr_dyn_release(item); return scr_dyn_new_num((double)index); } scr_dyn_release(item); } - return dyn_name_is(method, "findLast") + return (mid == DYN_M_findLast) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "join")) { + if ((mid == DYN_M_join)) { ScrJsonBuf b; scr_jb_init(&b); ScrStr *separator = argc > 0 && args[0]->kind != SCR_DYN_UNDEF @@ -1250,7 +1261,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return r; } - if (dyn_name_is(method, "concat")) { + if ((mid == DYN_M_concat)) { ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { if (!dyn_arr_copy_item(out, recv, i, true)) { scr_dyn_release(out); return NULL; } @@ -1271,7 +1282,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "flat")) { + if ((mid == DYN_M_flat)) { double depth = dyn_index_arg(args, argc, 0, 1, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1283,7 +1294,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "reverse")) { + if ((mid == DYN_M_reverse)) { for (size_t i = 0; i < len / 2; i++) { ScrDyn *tmp = recv->v.arr.items[i]; recv->v.arr.items[i] = recv->v.arr.items[len - 1 - i]; @@ -1291,12 +1302,12 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toReversed")) { + if ((mid == DYN_M_toReversed)) { ScrDyn *out = scr_dyn_new_arr(); if (!dyn_arr_reversed_copy(out, recv)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "with")) { + if ((mid == DYN_M_with)) { double indexD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1314,7 +1325,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "toSpliced")) { + if ((mid == DYN_M_toSpliced)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; size_t start = dyn_rel_index(startD, len); @@ -1331,7 +1342,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start + delete_count; i < len; i++) if (!dyn_arr_copy_item(out, recv, i, false)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "fill")) { + if ((mid == DYN_M_fill)) { double startD = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 2, (double)len, what); @@ -1359,7 +1370,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double targetD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double startD = dyn_index_arg(args, argc, 1, 0, what); @@ -1392,13 +1403,13 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || - dyn_name_is(method, "filter") || dyn_name_is(method, "some") || - dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex")) { + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0]; - ScrDyn *out = (dyn_name_is(method, "map") || dyn_name_is(method, "filter")) ? scr_dyn_new_arr() : NULL; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; /* Array iteration methods snapshot length before the first callback. * A shrinking callback can remove a later dense entry; an expanding * callback must not make the new tail observable to this pass. Live @@ -1413,30 +1424,30 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if (dyn_name_is(method, "map")) { + if ((mid == DYN_M_map)) { scr_dyn_arr_push(out, r); /* ownership moves in */ r = NULL; } else { bool truthy = scr_dyn_truthy(r); scr_dyn_release(r); - if (dyn_name_is(method, "filter") && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if (dyn_name_is(method, "some") && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if (dyn_name_is(method, "every") && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if (dyn_name_is(method, "find") && truthy) return item; /* +1 moves out */ - if (dyn_name_is(method, "findIndex") && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } } scr_dyn_release(item); } if (out) return out; - if (dyn_name_is(method, "some")) return scr_dyn_new_bool(false); - if (dyn_name_is(method, "every")) return scr_dyn_new_bool(true); - if (dyn_name_is(method, "find")) return scr_dyn_retain(scr_dyn_undefined()); - if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1); + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ } - if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) { + if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; - bool backward = dyn_name_is(method, "reduceRight"); + bool backward = (mid == DYN_M_reduceRight); ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t step = 0; step < len; step++) { @@ -1458,7 +1469,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if (dyn_name_is(method, "flatMap")) { + if ((mid == DYN_M_flatMap)) { /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 * flatten. Native dyn-array results flatten element-by-element; a * WRAPPED engine array flattens through the routed keyed reads @@ -1496,7 +1507,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "sort")) { + if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { /* V8 appends the received value's string image. */ @@ -1510,7 +1521,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL; return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toSorted")) { + if ((mid == DYN_M_toSorted)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { static const char message[] = "The comparison function must be either a function or undefined"; @@ -1525,7 +1536,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_arr_proto_unimpl(method)) { + if (dyn_arr_proto_unimpl(mid)) { dyn_throw_unsupported("Array", method); return NULL; } @@ -1541,14 +1552,14 @@ static ScrDyn *scr_dyn_invoke_impl( #ifdef SCR_LIB scr_trap("scriptc: promise dispatch is unavailable in library mode\n"); #else - if (dyn_name_is(method, "then")) { + if ((mid == DYN_M_then)) { return scr_dyn_promise_then(recv->v.promise, argc >= 1 ? args[0] : NULL, argc >= 2 ? args[1] : NULL, NULL); } - if (dyn_name_is(method, "catch")) { + if ((mid == DYN_M_catch)) { return scr_dyn_promise_then(recv->v.promise, NULL, argc >= 1 ? args[0] : NULL, NULL); } - if (dyn_name_is(method, "finally")) { + if ((mid == DYN_M_finally)) { return scr_dyn_promise_then(recv->v.promise, NULL, NULL, argc >= 1 ? args[0] : NULL); } dyn_throw_not_fn(what); @@ -1600,44 +1611,44 @@ static ScrDyn *scr_dyn_invoke_impl( } /* The typed array methods below are not DataView's; it inherits * Object.prototype's. */ - if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { + if ((mid == DYN_M_toString) || (mid == DYN_M_toLocaleString)) { ScrStr *tag = scr_str_new("[object DataView]", 17); ScrDyn *out = scr_dyn_new_str(tag); scr_str_release(tag); return out; } - if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + if ((mid == DYN_M_valueOf)) return scr_dyn_retain(recv); dyn_throw_not_fn(what); return NULL; } size_t blen = bytes->len; - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double idx = iD < 0 ? (double)blen + iD : iD; if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_new_num(scr_bytes_get(bytes, idx)); } - if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { + if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { /* subarray and Buffer.slice alias their source. TypedArray.slice * owns an independent copy; all keep the receiver's Buffer flavor. */ double startD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") + ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - if (dyn_name_is(method, "set")) { + if ((mid == DYN_M_set)) { double offset = dyn_bytes_index_arg(args, argc, 1, 0); if (scr_exc_pending()) return NULL; scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double target = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1647,7 +1658,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "fill") && !recv->buffer) { + if ((mid == DYN_M_fill) && !recv->buffer) { double value; if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1657,8 +1668,8 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) { - ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF + if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { + ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); if (scr_exc_pending()) { scr_str_release(separator); return NULL; } ScrStr *joined = scr_bytes_join(bytes, separator); @@ -1667,7 +1678,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } - if (dyn_bytes_proto_real(method)) { + if (dyn_bytes_proto_real(mid)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; } diff --git a/packages/runtime/src/scr_dyn_methods.h b/packages/runtime/src/scr_dyn_methods.h new file mode 100644 index 000000000..49e5c7254 --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.h @@ -0,0 +1,160 @@ +/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c + * uses. Do not edit by hand. */ +#ifndef SCR_DYN_METHODS_H +#define SCR_DYN_METHODS_H + +#include +#include + +enum { + DYN_M_NONE, + DYN_M_apply, + DYN_M_at, + DYN_M_bind, + DYN_M_call, + DYN_M_catch, + DYN_M_concat, + DYN_M_copyWithin, + DYN_M_entries, + DYN_M_every, + DYN_M_fill, + DYN_M_filter, + DYN_M_finally, + DYN_M_find, + DYN_M_findIndex, + DYN_M_findLast, + DYN_M_findLastIndex, + DYN_M_flat, + DYN_M_flatMap, + DYN_M_forEach, + DYN_M_includes, + DYN_M_indexOf, + DYN_M_join, + DYN_M_keys, + DYN_M_lastIndexOf, + DYN_M_map, + DYN_M_pop, + DYN_M_push, + DYN_M_reduce, + DYN_M_reduceRight, + DYN_M_reverse, + DYN_M_set, + DYN_M_shift, + DYN_M_slice, + DYN_M_some, + DYN_M_sort, + DYN_M_splice, + DYN_M_subarray, + DYN_M_then, + DYN_M_toExponential, + DYN_M_toFixed, + DYN_M_toLocaleString, + DYN_M_toReversed, + DYN_M_toSorted, + DYN_M_toSpliced, + DYN_M_toString, + DYN_M_unshift, + DYN_M_valueOf, + DYN_M_values, + DYN_M_with, + DYN_M_COUNT +}; + +static const char *const dyn_method_names[DYN_M_COUNT] = { + NULL, + "apply", + "at", + "bind", + "call", + "catch", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "finally", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "set", + "shift", + "slice", + "some", + "sort", + "splice", + "subarray", + "then", + "toExponential", + "toFixed", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "valueOf", + "values", + "with", +}; + +static const unsigned char dyn_method_lengths[DYN_M_COUNT] = { + 0, 5, 2, 4, 4, 5, 6, 10, 7, 5, 4, 6, 7, 4, 9, 8, + 13, 4, 7, 7, 8, 7, 4, 4, 11, 3, 3, 4, 6, 11, 7, 3, + 5, 5, 4, 4, 6, 8, 4, 13, 7, 14, 10, 8, 9, 8, 7, 7, + 6, 4, +}; + +/* Each name's id at its slot (or the next free one); 0 ends a probe. */ +static const unsigned char dyn_method_slots[256] = { + 0, 0, 0, 0, 12, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 14, 0, 0, 27, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 28, 3, 0, + 22, 0, 20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, + 0, 0, 0, 0, 0, 33, 31, 0, 0, 0, 0, 0, 36, 35, 0, 0, + 0, 0, 0, 0, 23, 4, 32, 0, 5, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 6, 0, 0, 37, 0, + 0, 40, 0, 0, 0, 0, 0, 0, 43, 0, 0, 45, 0, 0, 0, 44, + 0, 7, 0, 0, 0, 0, 42, 0, 25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, + 46, 0, 0, 39, 0, 41, 0, 9, 0, 0, 13, 0, 0, 0, 0, 8, + 0, 47, 10, 0, 0, 0, 0, 48, 0, 0, 17, 0, 0, 49, 0, 0, + 0, 0, 0, 19, 0, 0, 11, 0, 0, 0, 0, 18, 0, 0, 0, 0, +}; + +#define DYN_METHOD_MAX_LENGTH 14 + +/* The id of a method name, compared by content; DYN_M_NONE for any other + * name. */ +static inline int dyn_method_of(const char *m) { + size_t length = strlen(m); + if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE; + unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 + + (unsigned char)m[length - 1]) & 255; + for (;;) { + unsigned id = dyn_method_slots[slot]; + if (id == 0) return DYN_M_NONE; + if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id; + slot = (slot + 1) & 255; + } +} + +#endif diff --git a/packages/runtime/src/scr_dyn_methods.test.ts b/packages/runtime/src/scr_dyn_methods.test.ts new file mode 100644 index 000000000..77911aa2d --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.test.ts @@ -0,0 +1,10 @@ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; +import { expect, test } from "vitest"; + +test("scr_dyn_methods.h lists every method id the checked-dynamic dispatch compares", () => { + const script = join(import.meta.dirname, "../../../scripts/gen-dyn-methods.mjs"); + expect(() => + execFileSync(process.execPath, [script, "--check"], { stdio: "pipe" }), + ).not.toThrow(); +}); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 3d10183f4..a23395c91 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -354,17 +354,23 @@ static bool scr_worker_transfer_has(const ScrDyn *transfer, const ScrWorkerView return false; } +/* Whether a method name is `name`: first characters compare inline, so + * the dispatch ladders below call strcmp only for names that can match. */ +static inline bool method_is(const char *method, const char *name) { + return method[0] == name[0] && strcmp(method, name) == 0; +} + static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "hasRef")) + if (view->port && method_is(method, "hasRef")) return scr_dyn_new_bool(view->referenced && (!view->closed || (view->channel && view->closing))); - if (!view->port && !strcmp(method, "terminate")) { + if (!view->port && method_is(method, "terminate")) { if (!view->termination) { view->termination = scr_promise_new(); if (view->closed) scr_promise_fulfill_ref(view->termination, @@ -373,7 +379,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_new_promise(view->termination); } - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { /* Worker drops messages after exit before validating or cloning. A * closed MessagePort still validates its payload, as Node does. */ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined()); @@ -405,9 +411,9 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho else scr_worker_thread_post(view->thread, message); return scr_dyn_retain(scr_dyn_undefined()); } - bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener"); - bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener"); - if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) { + bool once = method_is(method, "once") || method_is(method, "prependOnceListener"); + bool prepend = method_is(method, "prependListener") || method_is(method, "prependOnceListener"); + if (once || prepend || method_is(method, "on") || method_is(method, "addListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -416,7 +422,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { + if (method_is(method, "off") || method_is(method, "removeListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -425,21 +431,21 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "removeAllListeners")) { + if (method_is(method, "removeAllListeners")) { bool all = !argc || args[0]->kind == SCR_DYN_UNDEF; if (!all && args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "listenerCount")) { + if (method_is(method, "listenerCount")) { if (!argc || args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1]) : scr_emitter_listener_count(view->events, args[0]->v.str)); } - if (view->port && !strcmp(method, "close")) { + if (view->port && method_is(method, "close")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { scr_dyn_check_listener(args[0], "callback"); if (scr_exc_pending()) return NULL; @@ -460,7 +466,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "start")) { + if (view->port && method_is(method, "start")) { view->started = true; return scr_dyn_retain(scr_dyn_undefined()); } @@ -1021,7 +1027,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { if (view->closed) { scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); return NULL; @@ -1045,7 +1051,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, scr_message_free(first); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "close")) { + if (method_is(method, "close")) { if (!view->closed) { view->closed = true; scr_broadcast_leave(view->channel); @@ -1053,8 +1059,8 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(self); } ScrDyn *out = NULL; diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs new file mode 100644 index 000000000..fe4f81a90 --- /dev/null +++ b/scripts/gen-dyn-methods.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the + * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the + * names, and an open-addressed hash table mapping a name to its id. The ids + * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the + * dispatch code. + * + * node scripts/gen-dyn-methods.mjs write the header + * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ +import { readFileSync, writeFileSync } from "node:fs"; + +const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); +const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); +const SLOTS = 256; + +const used = new Set(); +for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) + if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); +const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); + +// The slot dyn_method_of in the header computes. +const slotOf = (name) => + (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); +const slots = new Array(SLOTS).fill(0); +names.forEach((name, i) => { + let slot = slotOf(name); + while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); + slots[slot] = i + 1; +}); + +const rows = (values, width) => { + const out = []; + for (let i = 0; i < values.length; i += width) + out.push(` ${values.slice(i, i + width).join(", ")},`); + return out; +}; +const text = [ + "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", + " * uses. Do not edit by hand. */", + "#ifndef SCR_DYN_METHODS_H", + "#define SCR_DYN_METHODS_H", + "", + "#include ", + "#include ", + "", + "enum {", + " DYN_M_NONE,", + ...names.map((name) => ` DYN_M_${name},`), + " DYN_M_COUNT", + "};", + "", + "static const char *const dyn_method_names[DYN_M_COUNT] = {", + " NULL,", + ...names.map((name) => ` ${JSON.stringify(name)},`), + "};", + "", + "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", + ...rows([0, ...names.map((name) => name.length)], 16), + "};", + "", + `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, + `static const unsigned char dyn_method_slots[${SLOTS}] = {`, + ...rows(slots, 16), + "};", + "", + `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, + "", + "/* The id of a method name, compared by content; DYN_M_NONE for any other", + " * name. */", + "static inline int dyn_method_of(const char *m) {", + " size_t length = strlen(m);", + " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", + " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", + ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, + " for (;;) {", + " unsigned id = dyn_method_slots[slot];", + " if (id == 0) return DYN_M_NONE;", + " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", + ` slot = (slot + 1) & ${SLOTS - 1};`, + " }", + "}", + "", + "#endif", + "", +].join("\n"); + +if (process.argv.includes("--check")) { + let current = ""; + try { + current = readFileSync(target, "utf8"); + } catch {} + if (current !== text) { + console.error( + "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", + ); + process.exit(1); + } +} else { + writeFileSync(target, text); +} diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 3158de997..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,7 +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. 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. */ @@ -408,6 +409,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", + "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", From e5b2e62b45c16f9f87b5f4ca934526e8f7ab050f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:25:30 -0500 Subject: [PATCH 033/169] Emit function bodies on forked workers that merge into one emitter's text On hosts that can fork, the native compiler now emits a large module's function bodies on worker processes (SCRIPTC_EMIT_WORKERS, 4 by default). The function list splits into contiguous ranges, each emitted by a forked child of the emitter, and the ranges merge in function order into the text one emitter gives. A range whose worker fails is emitted in the parent. Modules under 2,000 functions, Node and Windows keep the serial path. A range's emitter records an ordered journal (emit-journal.ts): - every helper it numbers from a counter (the dyn conversions and matchers, message listeners and readers, JSON writers, publication walkers, stream adapters and resolve thunks), recorded when its number is taken, with the entry point and arguments that create it; - the first request of each key of a module-level table (declarations, literals, C strings, unit and absent instances, retype guards, regex and template-strings instances, map descriptors, alias tags) made outside a helper's definition; - unkeyed sites (inline caches, speculative loop sites) taken outside a definition. A speculative loop site's global is now `@sc_spec_bails_N`, named like the other numbered sites. The merging emitter replays each range's journal in order. Replay calls the same entry points, so every helper gets the number and definition the serial emission gives it, and table entries land in the serial order. The range's text is then renamed to the merged numbers in one pass. Sets and flags the ranges set (await callees, function values, RC helper requests) merge after the replay. LlvmTargetOptions.functionChunks runs the same merge in one process, for tests. The fork, exit, wait and lock primitives come from the native host (host.c, process-fork.ts); the native compiler passes them to the emitter as LlvmTargetOptions.forkPrimitives. --- packages/compiler/native/host.c | 60 +++ packages/compiler/native/host.ffi.json | 8 +- packages/compiler/src/backend/llvm/dyn.ts | 91 +++++ .../compiler/src/backend/llvm/emit-journal.ts | 238 ++++++++++++ .../compiler/src/backend/llvm/emit-module.ts | 149 +++++++- .../src/backend/llvm/emit-statements.ts | 3 +- packages/compiler/src/backend/llvm/emitter.ts | 360 +++++++++++++++++- .../src/backend/llvm/expr-callbacks.ts | 79 ++++ .../compiler/src/backend/llvm/expr-calls.ts | 44 ++- .../src/backend/llvm/expr-containers.ts | 5 + .../src/backend/llvm/expr-primitives.ts | 2 + .../src/backend/llvm/expr-stream-bridges.ts | 25 ++ .../src/backend/llvm/expr-stream-callbacks.ts | 42 ++ .../compiler/src/backend/llvm/lib-connect.ts | 7 + .../compiler/src/backend/llvm/lib-datagram.ts | 8 + .../compiler/src/backend/llvm/lib-http2.ts | 5 + .../compiler/src/backend/llvm/lib-lookup.ts | 28 +- packages/compiler/src/backend/llvm/publish.ts | 20 + packages/compiler/src/backend/llvm/shapes.ts | 8 + packages/compiler/src/backend/llvm/walkers.ts | 21 + .../compiler/src/frontend/process-fork.ts | 26 ++ packages/compiler/src/native/cli.ts | 16 + packages/compiler/src/native/prepare.ts | 24 +- .../compiler/test/chunked-emission.test.ts | 69 ++++ 24 files changed, 1302 insertions(+), 36 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/emit-journal.ts create mode 100644 packages/compiler/src/frontend/process-fork.ts create mode 100644 packages/compiler/test/chunked-emission.test.ts diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index 6aa1b2a28..afc645ab3 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -56,3 +56,63 @@ void scr_collect_cycles_phase(void); void scriptc_native_collect_cycles(void) { scr_collect_cycles_phase(); } + +/* Forked lowering workers (frontend/process-fork.ts). A worker shares the + * parent's heap copy-on-write, writes its results to a file and leaves with + * _exit, so no buffered output or exit handler of the parent runs twice. + * Checker requests from workers hold an exclusive flock on a file each + * worker opens after the fork, so every worker has its own open file + * description and the lock excludes the other workers. */ +#ifdef _WIN32 +int32_t scriptc_native_fork(void) { return -1; } +void scriptc_native_exit(int32_t code) { ExitProcess((UINT)code); } +int32_t scriptc_native_wait(int32_t pid) { (void)pid; return -1; } +uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { (void)bytes; (void)size; return 0; } +void scriptc_native_lock(void) {} +void scriptc_native_unlock(void) {} +#else +#include +#include +#include +#include + +int32_t scriptc_native_fork(void) { return (int32_t)fork(); } + +void scriptc_native_exit(int32_t code) { _exit(code); } + +/* The worker's exit status, 128 plus the signal for a worker a signal + * ended, or -1 when waitpid fails. */ +int32_t scriptc_native_wait(int32_t pid) { + int status = 0; + while (waitpid((pid_t)pid, &status, 0) < 0) { + if (errno != EINTR) return -1; + } + if (WIFEXITED(status)) return WEXITSTATUS(status); + if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); + return -1; +} + +static int scriptc_native_lock_fd = -1; + +uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { + if (size == 0 || memchr(bytes, 0, size) != NULL) return 0; + char *path = malloc(size + 1); + if (!path) return 0; + memcpy(path, bytes, size); + path[size] = 0; + if (scriptc_native_lock_fd >= 0) close(scriptc_native_lock_fd); + scriptc_native_lock_fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC, 0600); + free(path); + return scriptc_native_lock_fd >= 0; +} + +void scriptc_native_lock(void) { + if (scriptc_native_lock_fd < 0) return; + while (flock(scriptc_native_lock_fd, LOCK_EX) < 0 && errno == EINTR) { + } +} + +void scriptc_native_unlock(void) { + if (scriptc_native_lock_fd >= 0) flock(scriptc_native_lock_fd, LOCK_UN); +} +#endif diff --git a/packages/compiler/native/host.ffi.json b/packages/compiler/native/host.ffi.json index d7e0e92aa..2bad2a0ce 100644 --- a/packages/compiler/native/host.ffi.json +++ b/packages/compiler/native/host.ffi.json @@ -2,6 +2,12 @@ "ffi_format": 6, "functions": [ { "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" }, - { "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" } + { "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }, + { "name": "compilerNativeFork", "symbol": "scriptc_native_fork", "params": [], "returns": "i32" }, + { "name": "compilerNativeExit", "symbol": "scriptc_native_exit", "params": ["i32"], "returns": "void" }, + { "name": "compilerNativeWait", "symbol": "scriptc_native_wait", "params": ["i32"], "returns": "i32" }, + { "name": "compilerNativeLockOpen", "symbol": "scriptc_native_lock_open", "params": ["string"], "returns": "bool" }, + { "name": "compilerNativeLock", "symbol": "scriptc_native_lock", "params": [], "returns": "void" }, + { "name": "compilerNativeUnlock", "symbol": "scriptc_native_unlock", "params": [], "returns": "void" } ] } diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 5c9d8e60f..594634893 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -196,6 +196,55 @@ export class LlDyn { constructor(private readonly host: DynHost) {} + /** Journal replay (emit-journal.ts): builds or finds the helper a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + switch (op) { + case "dyn.collectionDynOps": + return this.collectionDynOps(args[0] as IrType & { kind: "map" | "set" }); + case "dyn.dynMatchHelper": + return this.dynMatchHelper(args[0] as IrType, args[1] as boolean); + case "dyn.jsonSchema": + return this.jsonSchema(args[0] as IrType); + case "dyn.recordNameTable": { + const shapeId = args[0] as string; + return this.recordNameTable(shapeId, this.host.recordsById.get(shapeId)!).names; + } + case "dyn.jsonParseHelper": + return this.jsonParseHelper(args[0] as IrType)!; + case "dyn.dynCheckHelper": + return this.dynCheckHelper(args[0] as IrType, args[1] as boolean); + case "dyn.toDynHelper": + return this.toDynHelper(args[0] as IrType); + case "dyn.promiseDynAdapterHelper": + return this.promiseDynAdapterHelper(args[0] as IrType); + case "dyn.dynFuncThunkHelper": + return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynFuncBoxHelper": + return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynFuncAdapterHelper": + return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" }); + case "dyn.dynToStrHelper": + return this.dynToStrHelper(); + case "dyn.dynIterNHelper": + return this.dynIterNHelper(); + case "dyn.dynDestrCheckHelper": + return this.dynDestrCheckHelper(); + case "dyn.dynKeyGetHelper": + return this.dynKeyGetHelper(); + case "dyn.dynComputedKeyGetHelper": + return this.dynComputedKeyGetHelper(); + case "dyn.caughtToDynHelper": + return this.caughtToDynHelper(); + case "dyn.messageListenerHelper": + return this.messageListenerHelper(args[0] as IrType & { kind: "func" }) ?? ""; + case "dyn.messageReaderHelper": + return this.messageReaderHelper(args[0] as IrType); + default: + throw new InternalCompilerError(`llvm emitter bug: no dyn helper replay ${op}`); + } + } + /** The types a dyn matcher or builder for `t` checks structurally: record * fields and index values, array elements and union arms. */ private structuralChildren(t: IrType): readonly IrType[] { @@ -385,6 +434,7 @@ export class LlDyn { if (prior) return prior; const name = `sc_map_dyn_${this.mapDynOps.size}`; this.mapDynOps.set(key, name); + this.host.journalAllocated?.(name, "dyn.collectionDynOps", [t]); const keyType = t.kind === "map" ? t.key : t.elem; const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" }; const emitSlot = (type: IrType, role: string): void => { @@ -442,6 +492,7 @@ export class LlDyn { `@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`, "", ); + this.host.journalBuilt?.(); return name; } @@ -840,6 +891,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dm_${this.dynMatchers.size}`; this.dynMatchers.set(matcherKey, name); + this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]); const recursive = this.isRecursiveShape(t); const wrapped = recursive || this.holdsRecursiveShape(t); const B = new BlockBuilder(); @@ -1200,6 +1252,7 @@ export class LlDyn { ``, ); if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit"); + this.host.journalBuilt?.(); return name; } @@ -1293,6 +1346,7 @@ export class LlDyn { const S = host.sizeType; const name = `sc_jsch_${this.jsonSchemas.size}`; this.jsonSchemas.set(key, name); + this.host.journalAllocated?.(name, "dyn.jsonSchema", [t]); let body: string; switch (t.kind) { case "f64": @@ -1364,6 +1418,7 @@ export class LlDyn { `@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -1453,6 +1508,7 @@ export class LlDyn { const base = `sc_rnames_${this.recordNameTables.size}`; const table = { names: `@${base}`, lens: `@${base}_lens` }; this.recordNameTables.set(shapeId, table); + this.host.journalAllocated?.(base, "dyn.recordNameTable", [shapeId]); const n = shape.fields.length; const names = recordSourceFieldOrder(shape); this.defs.push( @@ -1460,6 +1516,7 @@ export class LlDyn { `@${base}_lens = internal constant [${n} x ${S}] [${names.map((name) => `${S} ${Buffer.byteLength(name, "utf8")}`).join(", ")}]`, ``, ); + this.host.journalBuilt?.(); return table; } @@ -1478,6 +1535,7 @@ export class LlDyn { const host = this.host; const name = `sc_jp_${this.jsonParsers.size}`; this.jsonParsers.set(key, name); + this.host.journalAllocated?.(name, "dyn.jsonParseHelper", [t]); const schema = this.jsonSchema(t); const check = this.dynCheckHelper(t); host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`); @@ -1504,6 +1562,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -1519,6 +1578,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dc_${this.dynBuilders.size}`; this.dynBuilders.set(builderKey, name); + this.host.journalAllocated?.(name, "dyn.dynCheckHelper", [t, preserveRefs]); const recursive = this.isRecursiveShape(t); const memo = !recursive && this.holdsRecursiveShape(t) && this.valTy(t) === "ptr"; const host = this.host; @@ -1541,6 +1601,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } if (isRefCounted(t) && t.kind !== "dyn") { @@ -2537,6 +2598,7 @@ export class LlDyn { ``, ); if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo"); + this.host.journalBuilt?.(); return name; } @@ -2551,6 +2613,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_td_${this.toDynFns.size}`; this.toDynFns.set(key, name); + this.host.journalAllocated?.(name, "dyn.toDynHelper", [t]); const host = this.host; const B = new BlockBuilder(); let sourceAccessor: { name: string; release: string } | null = null; @@ -3157,6 +3220,7 @@ export class LlDyn { ``, ); } + this.host.journalBuilt?.(); return name; } @@ -3173,6 +3237,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_pda_${this.promiseDynAdapters.size}`; this.promiseDynAdapters.set(key, name); + this.host.journalAllocated?.(name, "dyn.promiseDynAdapterHelper", [inner]); const host = this.host; const B = new BlockBuilder(); host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); @@ -3250,6 +3315,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -3262,6 +3328,7 @@ export class LlDyn { if (this.dynToStrFn) return this.dynToStrFn; const name = "sc_ds"; this.dynToStrFn = name; + this.host.journalAllocated?.(name, "dyn.dynToStrHelper", []); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); host.declare(`declare ptr @scr_jb_finish(ptr)`); @@ -3566,6 +3633,7 @@ export class LlDyn { ``, ); } + this.host.journalBuilt?.(); return name; } @@ -3577,6 +3645,7 @@ export class LlDyn { if (this.caughtToDynFn) return this.caughtToDynFn; const name = "sc_cd"; this.caughtToDynFn = name; + this.host.journalAllocated?.(name, "dyn.caughtToDynHelper", []); const host = this.host; const B = new BlockBuilder(); const kp = B.tmp(); @@ -3682,6 +3751,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -3695,6 +3765,7 @@ export class LlDyn { this.helperMemo.set(name, name); const B = new BlockBuilder(); const host = this.host; + this.host.journalAllocated?.(name, "dyn.dynComputedKeyGetHelper", []); const stringHelper = this.dynKeyGetHelper(); host.declare(`declare ptr @scr_dyn_property_key_value(ptr)`); host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`); @@ -3732,6 +3803,7 @@ export class LlDyn { "}", "", ); + this.host.journalBuilt?.(); return name; } @@ -3741,6 +3813,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_key_get"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynKeyGetHelper", []); const host = this.host; const B = new BlockBuilder(); const kd = this.kindOf(B, "%d"); @@ -4134,6 +4207,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4145,6 +4219,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_destr_check"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynDestrCheckHelper", []); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); host.declare(`declare ptr @scr_jb_finish(ptr)`); @@ -4199,6 +4274,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4210,6 +4286,7 @@ export class LlDyn { if (existing) return existing; const name = "sc_dyn_iter_n"; this.dynBuilders.set(memoKey, name); + this.host.journalAllocated?.(name, "dyn.dynIterNHelper", []); const host = this.host; host.declare(`declare ptr @scr_dyn_new_arr()`); host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`); @@ -4414,6 +4491,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4445,6 +4523,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfk_${this.dynFuncThunks.size}`; this.dynFuncThunks.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncThunkHelper", [t]); const host = this.host; const B = new BlockBuilder(); const argNames: string[] = []; @@ -4594,6 +4673,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4604,6 +4684,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfb_${this.dynFuncBoxes.size}`; this.dynFuncBoxes.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncBoxHelper", [t]); const host = this.host; const thunk = this.dynFuncThunkHelper(t); host.declare(`declare ptr @scr_closure_retain_v(ptr)`); @@ -4618,6 +4699,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4640,11 +4722,15 @@ export class LlDyn { (t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") && this.messageReadable(param, new Set()); if (!supported) { + // A signature without an entry still takes a number. this.messageListeners.set(key, null); + this.host.journalAllocated?.("", "dyn.messageListenerHelper", [t]); + this.host.journalBuilt?.(); return null; } const name = `sc_dml_${this.messageListeners.size}`; this.messageListeners.set(key, name); + this.host.journalAllocated?.(name, "dyn.messageListenerHelper", [t]); const B = new BlockBuilder(); B.entryAllocas.push(`%fail = alloca i1`); B.line(`store i1 false, ptr %fail`); @@ -4676,6 +4762,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4737,6 +4824,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dmr_${this.messageReaders.size}`; this.messageReaders.set(key, name); + this.host.journalAllocated?.(name, "dyn.messageReaderHelper", [t]); const host = this.host; const S = host.sizeType; if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`); @@ -4909,6 +4997,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -4920,6 +5009,7 @@ export class LlDyn { if (existing) return existing; const name = `sc_dfa_${this.dynFuncAdapters.size}`; this.dynFuncAdapters.set(key, name); + this.host.journalAllocated?.(name, "dyn.dynFuncAdapterHelper", [t]); const host = this.host; const B = new BlockBuilder(); host.declare(`declare ptr @scr_box_get_ref(ptr)`); @@ -5022,6 +5112,7 @@ export class LlDyn { `}`, ``, ); + this.host.journalBuilt?.(); return name; } } diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts new file mode 100644 index 000000000..a778aa8f0 --- /dev/null +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -0,0 +1,238 @@ +/* Chunked function emission (emit-module.ts). An emitter that emits one + * contiguous range of a module's functions records here the helpers and + * sites it numbers from a counter (`sc_td_${size}` and the like). The + * emitter that merges the ranges replays each range's records in order: + * replay calls the same entry point with the same arguments, so every + * helper gets the number and the definition the serial emission gives it, + * and the range's text is renamed to those numbers. */ +import type { LlEmitter } from "./emitter.js"; + +/** One event of a range's emission, in order. */ +export type JournalEntry = + /** A helper or site the range's emitter numbered: the name it gave it + * (a counter prefix and a number), and the replay entry point + * (registerReplay) with its arguments, which are plain IR values. */ + | { + readonly kind: "op"; + readonly name: string; + readonly op: string; + readonly args: readonly unknown[]; + } + /** The first request of a key of a module-level table made outside any + * replayable definition, with the value the range's emitter stored. */ + | { + readonly kind: "entry"; + readonly table: string; + readonly key: string; + readonly value: unknown; + }; + +export class EmitJournal { + readonly entries: JournalEntry[] = []; + /** The replayable definitions being built, innermost last. */ + private readonly open: string[] = []; + + /** The definitions still open (empty between functions). */ + get unclosed(): readonly string[] { + return this.open; + } + /** Keys already recorded per table. */ + private readonly recorded = new Map>(); + + /** A keyed helper, recorded when its number is taken, before anything its + * definition creates in turn: that keeps the serial numbering order. + * Replaying a helper that exists answers its name, so helpers created + * inside another helper's definition are recorded too. Opens the + * definition, which the creating entry point closes with `built`. */ + allocated(name: string, op: string, args: readonly unknown[]): void { + this.entries.push({ kind: "op", name, op, args }); + this.open.push(op); + } + + /** Opens a definition whose helper is not replayed itself (its + * entry point records the request with `used`). */ + opened(op: string): void { + this.open.push(op); + } + + /** A request, made outside any definition, of the helper `name` that an + * entry point answered; recorded once per name. Replay makes the same + * request. */ + used(name: string, op: string, args: readonly unknown[]): void { + if (this.open.length !== 0) return; + if (this.usedNames.has(name)) return; + this.usedNames.add(name); + this.entries.push({ kind: "op", name, op, args }); + } + private readonly usedNames = new Set(); + + /** Closes the definition `allocated` or `opened` opened. */ + built(): void { + if (this.open.pop() === undefined) this.overclosed = true; + } + + /** A `built` closed a definition no `allocated` opened. */ + overclosed = false; + + /** An unkeyed site (one per use). Sites taken while a definition is built + * are taken again when the merging emitter replays that definition. */ + site(name: string, op: string, args: readonly unknown[] = []): void { + if (this.open.length === 0) this.entries.push({ kind: "op", name, op, args }); + } + + /** A request of `key` in a module-level table. Only the first request + * made outside any definition is recorded: requests made while a + * definition is built happen again when it is replayed. */ + entry(table: string, key: string, value: unknown): void { + if (this.open.length !== 0) return; + let keys = this.recorded.get(table); + if (keys === undefined) this.recorded.set(table, (keys = new Set())); + if (keys.has(key)) return; + keys.add(key); + this.entries.push({ kind: "entry", table, key, value }); + } +} + +/** A range emitter's result (LlEmitter.chunkResult): plain data, so a + * worker process can write it. */ +export interface ChunkResult { + readonly entries: readonly JournalEntry[]; + readonly flags: { + readonly usesCompactRefs: boolean; + readonly usesCoroutines: boolean; + readonly usesTimers: boolean; + readonly usesGenSlots: boolean; + readonly needsUnionNone: boolean; + readonly needsNullableAbsent: boolean; + readonly needsOom: boolean; + readonly needsBadTag: boolean; + readonly needsBadKey: boolean; + readonly needsRetainBox: boolean; + }; + readonly publishedNamesSym: string | null; + readonly rcHelpers: readonly string[]; + readonly immortalValues: readonly string[]; + readonly fnValues: readonly string[]; + readonly arraySearchKinds: readonly string[]; + readonly recordCloneShapes: readonly string[]; + readonly awaitCallees: readonly string[]; + readonly adoptCallees: readonly string[]; + readonly directAwaitCallees: readonly string[]; + readonly inlineAwaitCallees: readonly string[]; +} + +type Replay = (emitter: LlEmitter, args: readonly unknown[]) => string; +const replays = new Map(); + +/** Registers the entry point that creates (or finds) a numbered helper or + * site from its recorded arguments and answers its name. */ +export function registerReplay(op: string, replay: Replay): void { + replays.set(op, replay); +} + +/** Replays a range's entries on the merging emitter, in record order, and + * records the renames from the range's names to its own in `names`. */ +export function replayJournal( + emitter: LlEmitter, + entries: readonly JournalEntry[], + names: Map, + tags: Map, +): void { + for (const entry of entries) { + if (entry.kind === "entry") { + emitter.applyJournalEntry(entry.table, entry.key, entry.value, names, tags); + continue; + } + const replay = replays.get(entry.op); + if (replay === undefined) + throw new Error(`llvm emitter bug: no replay for journal entry ${entry.op}`); + const own = stripSigil(replay(emitter, entry.args)); + const local = stripSigil(entry.name); + if (own !== local) names.set(local, own); + } +} + +/** The rename of a range's text: numbered names (`names`, without the + * sigil) and field alias tags (`tags`). One scan for each kind of name: + * a numbered name starts `sc_` after a character that cannot be part of a + * name, and ends at the last digit after its counter prefix. */ +export function renameChunk( + names: ReadonlyMap, + tags: ReadonlyMap, +): (text: string) => string { + const prefixes = new Set(); + for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, "")); + if (prefixes.size === 0 && tags.size === 0) return (text) => text; + return (text) => { + let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes); + if (tags.size > 0) out = renameTags(out, tags); + return out; + }; +} + +function renameNames( + text: string, + names: ReadonlyMap, + prefixes: ReadonlySet, +): string { + const parts: string[] = []; + let copied = 0; + for (let at = text.indexOf("sc_"); at >= 0; at = text.indexOf("sc_", at + 3)) { + if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue; + // The counter prefix ends where the digits start; the number ends at + // the last digit. + let end = at + 3; + while (end < text.length && isNameChar(text.charCodeAt(end)) && !isDigit(text.charCodeAt(end))) + end++; + if (end === text.length || !isDigit(text.charCodeAt(end))) continue; + if (!prefixes.has(text.slice(at, end))) continue; + let last = end; + while (last < text.length && isDigit(text.charCodeAt(last))) last++; + const own = names.get(text.slice(at, last)); + if (own === undefined) continue; + parts.push(text.slice(copied, at), own); + copied = last; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + +function renameTags(text: string, tags: ReadonlyMap): string { + const marker = "!tbaa !"; + const parts: string[] = []; + let copied = 0; + for (let at = text.indexOf(marker); at >= 0; at = text.indexOf(marker, at + marker.length)) { + const start = at + marker.length; + let end = start; + while (end < text.length && isDigit(text.charCodeAt(end))) end++; + const own = tags.get(Number(text.slice(start, end))); + if (own === undefined) continue; + parts.push(text.slice(copied, start), String(own)); + copied = end; + } + if (copied === 0) return text; + parts.push(text.slice(copied)); + return parts.join(""); +} + +/** `[-$._A-Za-z0-9]`: a character of an LLVM identifier. */ +function isNameChar(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 || + code === 36 || + code === 45 || + code === 46 + ); +} + +function isDigit(code: number): boolean { + return code >= 48 && code <= 57; +} + +function stripSigil(name: string): string { + return name.startsWith("@") ? name.slice(1) : name; +} diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..75e6ad50f 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -2,6 +2,10 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; import { emitWasmModules } from "./wasm-modules.js"; import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js"; +import { type ChunkResult, EmitJournal } from "./emit-journal.js"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { JS_IC_WORDS } from "./js-ic.js"; import { mangleAsyncSpawn, @@ -59,16 +63,147 @@ import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; import { emitLibraryIdentityLines } from "../library-identity-markers.js"; import { InternalCompilerError } from "../../errors.js"; +/** The function bodies emitted in `chunks` contiguous ranges, each by its + * own emitter (LlEmitter.chunkEmitter), then merged in function order: the + * text one emitter gives. */ +function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] { + const functions = emitter.mod.functions; + const size = Math.ceil(functions.length / chunks); + const ranges: { chunk: LlEmitter; texts: string[] }[] = []; + for (let start = 0; start < functions.length; start += size) { + const chunk = emitter.chunkEmitter(); + const texts = functions.slice(start, start + size).map((fn) => { + const text = chunk.emitFunction(fn); + if (chunk.journal!.overclosed) + throw new Error(`llvm emitter bug: journal definition closed twice in ${fn.name}`); + if (chunk.journal!.unclosed.length > 0) + throw new Error( + `llvm emitter bug: unclosed journal definitions after ${fn.name}: ${chunk.journal!.unclosed.join(", ")}`, + ); + return text; + }); + ranges.push({ chunk, texts }); + } + const fnDefs: string[] = []; + for (const { chunk, texts } of ranges) { + // The plain-data result a worker process would write, round-tripped. + const result = JSON.parse(JSON.stringify(chunk.chunkResult())) as ChunkResult; + const rename = emitter.mergeChunk(result); + for (const text of texts) { + const def = rename(text); + emitter.fnDefText.set(functions[fnDefs.length]!.name, def); + fnDefs.push(def); + } + } + return fnDefs; +} + +/** Modules with fewer functions emit in one process: forking and merging + * would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS + * lowers it, for tests). */ +function workerMinFunctions(): number { + const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000"); + return Number.isFinite(value) && value > 0 ? value : 2000; +} + +/** The function bodies emitted on forked worker processes, `workers` at a + * time: the function list splits into contiguous ranges, each emitted in + * its own process, which inherits this emitter as it is at the fork and + * records a journal (emit-journal.ts). Ranges merge in function order, so + * the text is the serial text. A range whose worker fails is emitted here + * when its turn comes. Null without fork primitives (Node, Windows, inside + * a worker) or when the module is small. */ +function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | null { + const fork = emitter.targetOptions.forkPrimitives; + const functions = emitter.mod.functions; + if (fork === undefined || workers < 2) return null; + if (functions.length < workerMinFunctions()) return null; + // More ranges than workers: a range's merge overlaps the later ranges' + // emission, and a slow range holds back less. + const count = Math.min(functions.length, workers * 3); + const size = Math.ceil(functions.length / count); + const dir = mkdtempSync(join(tmpdir(), "scriptc-emit-")); + const pids: number[] = []; + const start = (range: number): void => { + const pid = fork.fork(); + if (pid === 0) { + let code = 1; + try { + emitter.journal = new EmitJournal(); + const texts: string[] = []; + for (const fn of functions.slice(range * size, (range + 1) * size)) { + texts.push(emitter.emitFunction(fn)); + relieveHeapPressure(); + } + writeFileSync(join(dir, `${range}.json`), JSON.stringify(emitter.chunkResult())); + // Function text never holds NUL, so it separates the functions. + writeFileSync(join(dir, `${range}.ll`), texts.join("\0")); + code = 0; + } catch { + // The parent emits the range itself. + } + fork.exit(code); + } + pids.push(pid); + }; + const fnDefs: string[] = []; + try { + for (let range = 0; range < Math.min(workers, count); range++) start(range); + for (let range = 0; range < count; range++) { + const pid = pids[range]!; + const status = pid > 0 ? fork.wait(pid) : -1; + // The next worker starts from this emitter as merged so far, which + // the journal allows: it records requests, not table contents. + const merged = status === 0 ? readRange(dir, range) : null; + const slice = functions.slice(range * size, (range + 1) * size); + if (merged !== null && merged.texts.length === slice.length) { + const rename = emitter.mergeChunk(merged.result); + slice.forEach((fn, index) => { + const def = rename(merged.texts[index]!); + emitter.fnDefText.set(fn.name, def); + fnDefs.push(def); + }); + } else + for (const fn of slice) { + const def = emitter.emitFunction(fn); + emitter.fnDefText.set(fn.name, def); + fnDefs.push(def); + relieveHeapPressure(); + } + if (pids.length < count) start(pids.length); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + return fnDefs; +} + +/** A worker's range result, or null when its files are missing. */ +function readRange(dir: string, range: number): { result: ChunkResult; texts: string[] } | null { + try { + const result = JSON.parse(readFileSync(join(dir, `${range}.json`), "utf8")) as ChunkResult; + const texts = readFileSync(join(dir, `${range}.ll`), "utf8").split("\0"); + return { result, texts }; + } catch { + return null; + } +} + export function emitModuleParts(emitter: LlEmitter): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. - const fnDefs: string[] = []; - for (const fn of emitter.mod.functions) { - const def = emitter.emitFunction(fn); - fnDefs.push(def); - emitter.fnDefText.set(fn.name, def); - relieveHeapPressure(); - } + const chunks = emitter.targetOptions.functionChunks ?? 1; + const fnDefs = + (chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? + emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ?? + []; + if (fnDefs.length === 0) + for (const fn of emitter.mod.functions) { + const def = emitter.emitFunction(fn); + fnDefs.push(def); + emitter.fnDefText.set(fn.name, def); + relieveHeapPressure(); + } emitter.selectStacklessAsync(fnDefs); const errorViews: string[] = []; for (const property of ["name", "message"]) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index dc91da2fb..e3998e782 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -2095,8 +2095,7 @@ export function emitLoopCopies( // count. After SPECULATION_BAILS bailing entries in a row, the site runs // the ordinary copy only. The count is a hint: both copies give the same // results, and a race between workers loses at most an update. - const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`; - emitter.declare(`${bails} = internal global i8 0`); + const bails = emitter.speculativeLoopSite(); const count = B.tmp(), trying = B.tmp(); B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..1ca9313b7 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -71,6 +71,13 @@ import { import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; +import { + type ChunkResult, + EmitJournal, + registerReplay, + renameChunk, + replayJournal, +} from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; @@ -392,9 +399,30 @@ export interface LlScopeEntry { lazy?: boolean; } +/** Process primitives for forked emission workers. */ +export interface ForkPrimitives { + /** 0 in the worker, the worker's pid in the parent, -1 on failure. */ + fork(): number; + /** Ends the worker without running exit handlers. */ + exit(code: number): void; + /** The worker's exit status, 128 plus a signal number, or -1. */ + wait(pid: number): number; +} + export interface LlvmTargetOptions { /** C ABI target; omitted direct emission uses the host ABI. */ targetTriple?: string; + /** Emit the function bodies in this many contiguous ranges, each by its + * own emitter, and merge them in function order (emit-journal.ts). The + * output is the same as one emitter's. */ + functionChunks?: number; + /** Emit the function bodies of a large module on this many forked worker + * processes at a time (emit-module.ts), forked with `forkPrimitives`. The + * output is the same as one emitter's. */ + functionWorkers?: number; + /** The host's process primitives, where it can fork (the native + * compiler's). */ + forkPrimitives?: ForkPrimitives | undefined; /** Exact frontend sources for dev-build locations and variable metadata. */ debugSources?: ReadonlyMap; /** Pointer width of the target C ABI. Native targets are 64-bit today. */ @@ -660,6 +688,7 @@ export class LlEmitter { tag = 2 + this.fieldAliasTags.size * 2; this.fieldAliasTags.set(field, tag); } + this.journal?.entry("tag", field, tag); this.fieldPointerTags.set(ptr, tag); } @@ -672,6 +701,7 @@ export class LlEmitter { tag = 2 + this.fieldAliasTags.size * 2; this.fieldAliasTags.set(category, tag); } + this.journal?.entry("tag", category, tag); return `, !tbaa !${tag}`; } @@ -1223,13 +1253,27 @@ export class LlEmitter { readonly chainSlots = new Map(); logArgSlots = 0; readonly stackTraces: boolean; + /** The options this emitter was built with (chunk emitters reuse them). */ + readonly targetOptions: LlvmTargetOptions; + /** Records the module-level effects of emitting a range of functions, + * which the emitter that merges the range replays (emit-journal.ts). Null + * when this emitter emits the whole module itself. */ + journal: EmitJournal | null; + /** The emitter's hosts forward to `journal` (journal set, or workers). */ + private readonly journaled: boolean; /** Stack-trace mode: functions no capture can include (no frame push). */ readonly unframedFunctions: ReadonlySet; constructor( readonly mod: IrModule, options: LlvmTargetOptions, + journal: EmitJournal | null = null, ) { + this.targetOptions = options; + this.journal = journal; + this.journaled = + journal !== null || + ((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined); this.stackTraces = observesStacks(mod); this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined); this.mockTimers = this.runtimeFeatures.mockTimers; @@ -1476,6 +1520,20 @@ export class LlEmitter { } } this.shapeHost = { + // Journal hooks only on emitters that can emit ranges (chunk + // emitters, and emitters whose workers take a journal after the + // fork), so the host's optional calls cost nothing otherwise. + journalAllocated: !this.journaled + ? undefined + : (name, op, args) => this.journal?.allocated(name, op, args), + journalBuilt: !this.journaled ? undefined : () => this.journal?.built(), + journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op), + journalUsed: !this.journaled + ? undefined + : (name, op, args) => this.journal?.used(name, op, args), + journalEntry: !this.journaled + ? undefined + : (table, key, value) => this.journal?.entry(table, key, value), sizeType: this.sizeType, cycleBufOffset: this.cycleBufOffset, tracedShapes: this.tracedShapes, @@ -2348,6 +2406,7 @@ export class LlEmitter { }; this.literals.set(text, lit); } + this.journal?.entry("literal", text, lit); const name = `@${lit.sym}`; this.immortalValues.add(name); return name; @@ -2358,6 +2417,7 @@ export class LlEmitter { literalEquals(text: string): string { const literal = this.internLiteral(text); const name = `@sc_streq_${literal.slice("@sc_lit_".length)}`; + this.journal?.entry("literalEquality", text, name); if (!this.literalEqualities.has(text)) { this.literalEqualities.set(text, name); if (Buffer.byteLength(text, "utf8") > INLINE_LITERAL_BYTES) { @@ -2384,6 +2444,7 @@ export class LlEmitter { lit = { sym: this.contentSymbol("sc_bigint_", key), value }; this.bigintLiterals.set(key, lit); } + this.journal?.entry("bigint", key, { sym: lit.sym, value: lit.value.toString() }); const name = `@${lit.sym}`; this.immortalValues.add(name); return name; @@ -2397,6 +2458,7 @@ export class LlEmitter { c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") }; this.cstrs.set(text, c); } + this.journal?.entry("cstr", text, c); return `@${c.sym}`; } @@ -2413,6 +2475,7 @@ export class LlEmitter { sym = this.contentSymbol("sc_map_ops_", body); this.mapOpsConsts.set(body, sym); } + this.journal?.entry("mapOps", body, sym); return `@${sym}`; } @@ -2439,6 +2502,7 @@ export class LlEmitter { sym = `sc_unit_${this.unitInstances.size}`; this.unitInstances.set(key, sym); } + this.journal?.entry("unit", key, sym); const name = `@${sym}`; this.immortalValues.add(name); return name; @@ -2452,6 +2516,7 @@ export class LlEmitter { sym = `sc_retype_guard_${this.retypeGuards.size}`; this.retypeGuards.set(unionId, sym); } + this.journal?.entry("retype", unionId, sym); return `@${sym}`; } @@ -2473,6 +2538,7 @@ export class LlEmitter { sym = `sc_absent_${this.absentInstances.size}`; this.absentInstances.set(unionId, sym); } + this.journal?.entry("absent", unionId, sym); const name = `@${sym}`; this.immortalValues.add(name); return name; @@ -2520,6 +2586,7 @@ export class LlEmitter { } declare(decl: string): void { + this.journal?.entry("decl", decl, null); this.decls.add(decl); } @@ -3640,6 +3707,7 @@ export class LlEmitter { const meta = this.classMetaOf(className); this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); } + this.journal?.entry("classObj", className, this.classObjs.get(className)); return mangleClassObj(className); } @@ -4347,6 +4415,191 @@ export class LlEmitter { return emitFunction(this, fn); } + /** What this range emitter (chunkEmitter) hands the merging emitter, as + * plain data a worker process can write: its journal and the sets and + * flags its functions set. */ + chunkResult(): ChunkResult { + return { + entries: this.journal!.entries, + flags: { + usesCompactRefs: this.usesCompactRefs, + usesCoroutines: this.usesCoroutines, + usesTimers: this.usesTimers, + usesGenSlots: this.usesGenSlots, + needsUnionNone: this.needsUnionNone, + needsNullableAbsent: this.needsNullableAbsent, + needsOom: this.needsOom, + needsBadTag: this.needsBadTag, + needsBadKey: this.needsBadKey, + needsRetainBox: this.needsRetainBox, + }, + publishedNamesSym: this.publishedNamesSym, + rcHelpers: this.rcHelpers === null ? [] : [...this.rcHelpers], + immortalValues: [...this.immortalValues], + fnValues: [...this.fnValues], + arraySearchKinds: [...this.arraySearchKinds], + recordCloneShapes: [...this.recordCloneShapes], + awaitCallees: [...this.awaitCallees], + adoptCallees: [...this.adoptCallees], + directAwaitCallees: [...this.directAwaitCallees], + inlineAwaitCallees: [...this.inlineAwaitCallees], + }; + } + + /** Merges a range emitter's result (chunkResult) into this emitter, in + * the range's order, and answers the rename of the range's text to this + * emitter's numbered helpers. Ranges merge in function order. */ + mergeChunk(chunk: ChunkResult): (text: string) => string { + const names = new Map(); + const tags = new Map(); + // The range's helpers and table entries first, in its order; then the + // sets and flags, which replayed definitions must find as serial + // emission left them (a helper set guards the declarations its first + // request adds). + replayJournal(this, chunk.entries, names, tags); + const flags = chunk.flags; + this.usesCompactRefs ||= flags.usesCompactRefs; + this.usesCoroutines ||= flags.usesCoroutines; + this.usesTimers ||= flags.usesTimers; + this.usesGenSlots ||= flags.usesGenSlots; + this.needsUnionNone ||= flags.needsUnionNone; + this.needsNullableAbsent ||= flags.needsNullableAbsent; + this.needsOom ||= flags.needsOom; + this.needsBadTag ||= flags.needsBadTag; + this.needsBadKey ||= flags.needsBadKey; + this.needsRetainBox ||= flags.needsRetainBox; + this.publishedNamesSym ??= chunk.publishedNamesSym; + if (this.rcHelpers !== null) for (const key of chunk.rcHelpers) this.rcHelpers.add(key); + for (const name of chunk.immortalValues) this.immortalValues.add(name); + for (const name of chunk.fnValues) this.fnValues.add(name); + for (const kind of chunk.arraySearchKinds) this.arraySearchKinds.add(kind as ArraySearchKind); + for (const shapeId of chunk.recordCloneShapes) this.recordCloneShapes.add(shapeId); + for (const name of chunk.awaitCallees) this.awaitCallees.add(name); + for (const name of chunk.adoptCallees) this.adoptCallees.add(name); + for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name); + for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name); + return renameChunk(names, tags); + } + + /** Adds a content-named constant a chunk interned. Its symbol is this + * emitter's own: a digest collision retries in first-use order, which a + * chunk alone does not know. */ + private addContentEntry( + into: Map, + key: string, + entry: V, + prefix: string, + names: Map, + ): void { + let own = into.get(key); + if (own === undefined) { + own = { ...entry, sym: this.contentSymbol(prefix, key) }; + into.set(key, own); + } + if (own.sym !== entry.sym) names.set(entry.sym, own.sym); + } + + /** Applies a chunk's table entry (EmitJournal.entry) at its place in the + * chunk's order: a key this emitter lacks is added, numbered after its + * own entries where the table numbers them, and the chunk's name for it + * is renamed to this emitter's. */ + applyJournalEntry( + table: string, + key: string, + value: unknown, + names: Map, + tags: Map, + ): void { + switch (table) { + case "decl": + this.decls.add(key); + return; + case "literal": + this.addContentEntry( + this.literals, + key, + value as { sym: string; len: number }, + "sc_lit_", + names, + ); + return; + case "literalEquality": + addEntry(this.literalEqualities, key, value as string); + return; + case "bigint": + { + const entry = value as { sym: string; value: string }; + this.addContentEntry( + this.bigintLiterals, + key, + { sym: entry.sym, value: BigInt(entry.value) }, + "sc_bigint_", + names, + ); + } + return; + case "cstr": + this.addContentEntry( + this.cstrs, + key, + value as { sym: string; len: number }, + "sc_cs_", + names, + ); + return; + case "mapOps": { + let own = this.mapOpsConsts.get(key); + if (own === undefined) { + own = this.contentSymbol("sc_map_ops_", key); + this.mapOpsConsts.set(key, own); + } + if (own !== value) names.set(value as string, own); + return; + } + case "classObj": + addEntry(this.classObjs, key, value as { nameSym: string }); + return; + case "retainWrapper": + addEntry(this.nullableUnions.retainWrappers, key, value as string); + return; + case "unit": + return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names); + case "absent": + return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names); + case "retype": + return addNumbered(this.retypeGuards, key, value as string, "sc_retype_guard_", names); + case "slots": + return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names); + case "regex": + return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names); + case "templateStrings": + return addNumberedSymbol( + this.templateStringsInstances, + key, + value as never, + "sc_tsa_", + names, + ); + case "tag": { + let own = this.fieldAliasTags.get(key); + if (own === undefined) { + own = 2 + this.fieldAliasTags.size * 2; + this.fieldAliasTags.set(key, own); + } + if (own !== value) tags.set(value as number, own); + return; + } + default: + throw new InternalCompilerError(`llvm emitter bug: no journal table ${table}`); + } + } + + /** An emitter for a range of this module's functions: the same facts, + * empty module-level tables, and a journal of what the range adds. */ + chunkEmitter(): LlEmitter { + return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal()); + } + /** The double-ABI entry of a function with an i32 entry: every caller * that is not a direct call (closures, dispatch, adapters, the runtime) * enters here. A pending exception passes through to its caller. */ @@ -5429,6 +5682,7 @@ export class LlEmitter { sym = `@sc_slots_${this.threadSlotsBlocks.size}`; this.threadSlotsBlocks.set(name, sym); } + this.journal?.entry("slots", name, sym); return sym; } @@ -5578,6 +5832,20 @@ export class LlEmitter { if (this.publishWalkersCache !== null) return this.publishWalkersCache; const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre); this.publishWalkersCache = new PublishWalkers({ + // Journal hooks only on emitters that can emit ranges (chunk + // emitters, and emitters whose workers take a journal after the + // fork), so the host's optional calls cost nothing otherwise. + journalAllocated: !this.journaled + ? undefined + : (name, op, args) => this.journal?.allocated(name, op, args), + journalBuilt: !this.journaled ? undefined : () => this.journal?.built(), + journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op), + journalUsed: !this.journaled + ? undefined + : (name, op, args) => this.journal?.used(name, op, args), + journalEntry: !this.journaled + ? undefined + : (table, key, value) => this.journal?.entry(table, key, value), declare: (decl) => this.declare(decl), cstr: (text) => this.cstr(text), sizeType: this.sizeType, @@ -6064,12 +6332,27 @@ export class LlEmitter { } jsIcSite(): string { - return `@sc_js_ic_${this.jsIcSiteCount++}`; + const name = `@sc_js_ic_${this.jsIcSiteCount++}`; + this.journal?.site(name, "jsIcSite"); + return name; } /** A fresh read-site cache for one constant-key dyn read. */ dynReadIcSite(): string { - return `@sc_dyn_read_ic_${this.dynReadIcSites++}`; + const name = `@sc_dyn_read_ic_${this.dynReadIcSites++}`; + this.journal?.site(name, "dynReadIcSite"); + return name; + } + + /** A fresh speculative loop site: the global that counts its bailing + * entries in a row. */ + speculativeLoopSite(): string { + const name = `@sc_spec_bails_${this.speculativeLoopSites++}`; + this.journal?.site(name, "speculativeLoopSite"); + this.journal?.opened("speculativeLoopSite"); + this.declare(`${name} = internal global i8 0`); + this.journal?.built(); + return name; } dynKind(d: string): string { @@ -6471,3 +6754,76 @@ export function adoptingAwaitOf(value: IrExpr | null): ExprOf<"awaitExpr"> | nul }); return found; } + +/** Adds `key` to `into` when it lacks it. */ +function addEntry(into: Map, key: string, value: V): void { + if (!into.has(key)) into.set(key, value); +} + +/** Adds `key` to a numbered table when it lacks it, numbering it after the + * table's own entries (`prefix` and its index), and records the rename of + * the chunk's `name` for it. */ +function addNumbered( + into: Map, + key: string, + name: string, + prefix: string, + names: Map, +): void { + let own = into.get(key); + if (own === undefined) { + own = `${prefix}${into.size}`; + into.set(key, own); + } + if (own !== name) names.set(name.replace(/^@/, ""), own.replace(/^@/, "")); +} + +/** addNumbered for entries that keep their numbered name in `sym`. */ +function addNumberedSymbol( + into: Map, + key: string, + entry: V, + prefix: string, + names: Map, +): void { + let own = into.get(key); + if (own === undefined) { + own = { ...entry, sym: `${prefix}${into.size}` }; + into.set(key, own); + } + if (own.sym !== entry.sym) names.set(entry.sym, own.sym); +} + +registerReplay("jsIcSite", (emitter) => emitter.jsIcSite()); +registerReplay("dynReadIcSite", (emitter) => emitter.dynReadIcSite()); +registerReplay("speculativeLoopSite", (emitter) => emitter.speculativeLoopSite()); +for (const op of [ + "dyn.collectionDynOps", + "dyn.dynMatchHelper", + "dyn.jsonSchema", + "dyn.recordNameTable", + "dyn.jsonParseHelper", + "dyn.dynCheckHelper", + "dyn.toDynHelper", + "dyn.promiseDynAdapterHelper", + "dyn.dynFuncThunkHelper", + "dyn.dynFuncBoxHelper", + "dyn.dynFuncAdapterHelper", + "dyn.dynToStrHelper", + "dyn.dynIterNHelper", + "dyn.dynDestrCheckHelper", + "dyn.dynKeyGetHelper", + "dyn.dynComputedKeyGetHelper", + "dyn.caughtToDynHelper", + "dyn.messageListenerHelper", + "dyn.messageReaderHelper", +]) + registerReplay(op, (emitter, args) => emitter.dyn.replayHelper(op, args)); +for (const op of [ + "walkers.jsonWriteHelper", + "walkers.recordFieldWriter", + "walkers.unionJoinHelper", +]) + registerReplay(op, (emitter, args) => emitter.walkers.replayHelper(op, args)); +for (const op of ["publish.walker", "publish.refusal"]) + registerReplay(op, (emitter, args) => emitter.publishWalkers.replayHelper(op, args)); diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index b91cdc543..398aebb21 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -14,6 +14,7 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; +import { registerReplay } from "./emit-journal.js"; /** Text-template union construction around an owned (+1) reference payload: * a fresh box, or the payload itself for a nullable union. */ @@ -55,6 +56,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp if (sym) return sym; sym = `sc_race_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "raceAdapterFor", [from, to]); const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : []; if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) { // A Promise entry (the frontend admits void entries without an @@ -70,6 +72,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp `}`, ``, ); + host.journal?.built(); return sym; } if (to.kind !== "union") @@ -98,6 +101,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp const tag = tagOf(UNDEFINED_T); d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } if (from.kind !== "union") { @@ -138,6 +142,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp } d.push(` ${fulfill("%u")}`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } // Sub-union re-tag: switch over the entry's arms, rebuild under the @@ -200,6 +205,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -213,6 +219,7 @@ export function genResultThunkFor( if (sym) return sym; sym = mangleGenResThunk(host.resolveThunks.size); host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]); const shape = host.recordsById.get(recT.shapeId); const base = host.nullableUnions.fieldBase(recT.shapeId); const valueT = shape?.fields.find((f) => f.name === "value")?.type; @@ -299,6 +306,7 @@ export function genResultThunkFor( ``, ]; host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } if (valueT.kind !== "union") throw new LlvmUnsupportedError(`genResume:${valueT.kind}`); @@ -456,6 +464,7 @@ export function genResultThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -467,6 +476,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri if (sym) return sym; sym = `sc_cx_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childExitThunkFor", [param]); const def = host.unionsById.get(param.unionId); const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; @@ -494,6 +504,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -510,6 +521,7 @@ export function childExitSignalThunkFor( if (sym) return sym; sym = `sc_cx_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]); const codeDef = host.unionsById.get(codeParam.unionId); const f64Tag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "f64") : -1; const codeNullTag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "nullT") : -1; @@ -560,6 +572,7 @@ export function childExitSignalThunkFor( `}`, ``, ); + host.journal?.built(); return sym; } @@ -569,6 +582,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_exec_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "execFileThunkFor", [cbT]); const errorParam = cbT.params[0]; let errorTag = -1; let nullTag = -1; @@ -627,6 +641,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -639,6 +654,7 @@ export function ipcMessageThunkFor( if (sym) return sym; sym = `sc_ipc_msg_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]); const param = cbT.params[0]; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %message) ${FN_ATTRS} { ; IPC message ${typeKey(cbT)}`, @@ -679,6 +695,7 @@ export function ipcMessageThunkFor( } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -688,6 +705,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_ipc_send_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]); const param = cbT.params[0]; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %error) ${FN_ATTRS} { ; IPC send callback ${typeKey(cbT)}`, @@ -736,6 +754,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -747,6 +766,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri if (sym) return sym; sym = `sc_cd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childDataThunkFor", [param]); const def = host.unionsById.get(param.unionId); const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1; if (bytesTag < 0) @@ -768,6 +788,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -780,6 +801,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | if (sym) return sym; sym = `sc_cdd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]); const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str"; host.declare(`declare ptr @${box}(ptr)`); host.resolveThunkDefs.push( @@ -793,6 +815,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | `}`, ``, ); + host.journal?.built(); return sym; } @@ -813,6 +836,7 @@ export function emitterFixedAdapter( if (sym) return { fn: sym, shim }; sym = `sc_ee_ad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); const params = cursor ? "ptr %args" : cbT.params.map((_, i) => `ptr %a${i}`).join(", "); @@ -857,6 +881,7 @@ export function emitterFixedAdapter( } d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return { fn: sym, shim }; } @@ -882,6 +907,7 @@ export function emitterOnceAdapter( if (sym) return { fn: sym, shim }; sym = `sc_ee_once_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); host.declare(`declare void @${fire}(ptr, ptr)`); @@ -921,6 +947,7 @@ export function emitterOnceAdapter( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return { fn: sym, shim }; } @@ -990,6 +1017,7 @@ export function closeBindThunkFor( if (sym) return sym; sym = `sc_ncb_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]); const def = host.unionsById.get(cbUnion.unionId); const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; const funcArm = funcTag >= 0 ? (def!.arms[funcTag] as IrType & { kind: "func" }) : null; @@ -1079,6 +1107,7 @@ export function closeBindThunkFor( } d.push(`}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -1094,6 +1123,7 @@ export function closeOverrideWrapFor( if (sym) return sym; sym = `sc_ncw_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]); const def = host.unionsById.get(cbUnion.unionId); const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; if (undefTag < 0) @@ -1122,6 +1152,7 @@ export function closeOverrideWrapFor( `}`, ``, ); + host.journal?.built(); return sym; } @@ -1140,6 +1171,7 @@ export function settledStoreFor( if (sym) return sym; sym = `sc_settled_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "settledStoreFor", [from, to]); const def = host.unionsById.get(to.unionId); if (!def) throw new InternalCompilerError("llvm emitter bug: settled store to an unknown union"); const armOf = (field: string): { tag: number; arm: IrType & { kind: "record" } } => { @@ -1230,5 +1262,52 @@ export function settledStoreFor( `}`, ``, ); + host.journal?.built(); return sym; } + +registerReplay("raceAdapterFor", (host, args) => + raceAdapterFor(host, args[0] as IrType, args[1] as IrType), +); +registerReplay("genResultThunkFor", (host, args) => + genResultThunkFor( + host, + args[0] as IrType & { kind: "generator" }, + args[1] as IrType & { kind: "record" }, + ), +); +registerReplay("childExitThunkFor", (host, args) => childExitThunkFor(host, args[0] as IrType)); +registerReplay("childExitSignalThunkFor", (host, args) => + childExitSignalThunkFor(host, args[0] as IrType, args[1] as IrType), +); +registerReplay("execFileThunkFor", (host, args) => + execFileThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("ipcMessageThunkFor", (host, args) => + ipcMessageThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("ipcSendThunkFor", (host, args) => + ipcSendThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("childDataThunkFor", (host, args) => childDataThunkFor(host, args[0] as IrType)); +registerReplay("childDataDynThunkFor", (host, args) => + childDataDynThunkFor(host, args[0] as "bytes" | "str"), +); +registerReplay("closeBindThunkFor", (host, args) => + closeBindThunkFor(host, args[0] as IrType, args[1] as boolean), +); +registerReplay("closeOverrideWrapFor", (host, args) => + closeOverrideWrapFor(host, args[0] as IrType, args[1] as boolean), +); +registerReplay("settledStoreFor", (host, args) => + settledStoreFor(host, args[0] as IrType, args[1] as IrType & { kind: "union" }), +); +registerReplay( + "emitterFixedAdapter", + (host, args) => emitterFixedAdapter(host, args[0] as IrType & { kind: "func" }).fn, +); +registerReplay( + "emitterOnceAdapter", + (host, args) => + emitterOnceAdapter(host, args[0] as IrType & { kind: "func" }, args[1] as string).fn, +); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 4bcf2a3d5..a4e7cde2c 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -11,6 +11,7 @@ import { type IrExpr, type IrFunction, classValueConstructor, + type IrType, } from "../../ir/ir.js"; import { liveRefView } from "./expr-stream-bridges.js"; import { typedRefConstructor, vAdapters } from "./shapes.js"; @@ -55,6 +56,7 @@ import { import type { Writes } from "./reference-writes.js"; import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js"; import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js"; +import { registerReplay } from "./emit-journal.js"; export function emitCallExpr( host: LlvmEmitterContext, @@ -815,22 +817,7 @@ export function emitCallExpr( const pushReceiver = (): void => { if (lazyReceiver) { const t = lazyReceiver.type; - const key = typeKey(t); - let recipe = host.typedRefRecipes.get(key); - if (recipe === undefined) { - const adapter = host.liveDynRefAdapter(t); - const rc = vAdapters(host.shapeHost, t); - const ctor = typedRefConstructor(host.shapeHost, t); - const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0; - // The box binds the same live view eager boxing attaches, so - // `in`, for-in and Object.keys see the class members. - const view = liveRefView(adapter); - recipe = `@sc_trr_${host.typedRefRecipes.size}`; - host.typedRefRecipes.set(key, recipe); - host.resolveThunkDefs.push( - `${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`, - ); - } + const recipe = typedRefRecipe(host, t); host.declare("declare void @scr_dyn_this_push_typed(ptr, ptr)"); B.line(`call void @scr_dyn_this_push_typed(ptr ${lazyReceiver.name}, ptr ${recipe})`); return; @@ -1493,3 +1480,28 @@ function emitNumericIndexRead( B.line(`${result} = phi ptr [ ${fast}, %${fastLabel} ], [ ${read.name}, %${slowLabel} ]`); return host.own({ name: result, type: e.type }); } + +/** The recipe a lazily boxed receiver of type `t` is built from: its RC + * adapters, type key and live view, one per type. */ +function typedRefRecipe(host: LlvmEmitterContext, t: IrType): string { + const key = typeKey(t); + let recipe = host.typedRefRecipes.get(key); + if (recipe !== undefined) return recipe; + const adapter = host.liveDynRefAdapter(t); + const rc = vAdapters(host.shapeHost, t); + const ctor = typedRefConstructor(host.shapeHost, t); + const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0; + // The box binds the same live view eager boxing attaches, so `in`, + // for-in and Object.keys see the class members. + const view = liveRefView(adapter); + recipe = `@sc_trr_${host.typedRefRecipes.size}`; + host.typedRefRecipes.set(key, recipe); + host.journal?.allocated(recipe, "typedRefRecipe", [t]); + host.resolveThunkDefs.push( + `${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`, + ); + host.journal?.built(); + return recipe; +} + +registerReplay("typedRefRecipe", (host, args) => typedRefRecipe(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8970aa3be..1d42c4730 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -58,6 +58,7 @@ import { emitDenseArrayPush, } from "./dense-array-access.js"; import { emitUnionWiden } from "./expr-records.js"; +import { registerReplay } from "./emit-journal.js"; export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string { const key = typeKey(inner); @@ -65,6 +66,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string if (!sym) { sym = mangleResolveThunk(host.resolveThunks.size); host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "resolveThunkFor", [inner]); const v = vAdapters(host.shapeHost, inner); host.declare(`declare void @scr_resolve_ref_impl(ptr, ptr, ptr, ptr, ptr)`); host.resolveThunkDefs.push( @@ -75,6 +77,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string `}`, ``, ); + host.journal?.built(); } return sym; } @@ -1539,3 +1542,5 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew } return out; } + +registerReplay("resolveThunkFor", (host, args) => resolveThunkFor(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 354004e9e..6f960c11f 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -563,6 +563,7 @@ export function regexLiteralTemplate( }; host.regexInstances.set(key, re); } + host.journal?.entry("regex", key, re); return `@${re.sym}`; } @@ -823,6 +824,7 @@ export function emitStringExpr( }; host.templateStringsInstances.set(e.key, inst); } + host.journal?.entry("templateStrings", e.key, inst); return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type }); } case "regexIntrinsic": diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a0a0cf24a..7cb0fa772 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -44,6 +44,7 @@ import type { NullableRefField } from "./nullable-fields.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; import { recordMayKeepSource } from "./record-sources.js"; +import { registerReplay } from "./emit-journal.js"; /** An owned (+1) union value for a nullable-pointer class field slot `p` * (the unit arm's immortal instance, or a fresh box around a retained @@ -91,6 +92,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri if (existing) return existing; const sym = `sc_dpa_${host.dynPromiseAdapters.size}`; host.dynPromiseAdapters.set(key, sym); + host.journal?.allocated(sym, "dynPromiseAdapter", [inner]); host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); host.declare(`declare void @scr_promise_reject_pending(ptr)`); @@ -146,6 +148,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri `}`, ``, ); + host.journal?.built(); return sym; } @@ -602,6 +605,7 @@ export function liveDynUnionRefAdapter( const sym = `sc_ldu_${host.liveDynUnionRefAdapters.size}`; host.liveDynUnionRefAdapters.set(key, sym); + host.journal?.allocated(sym, "liveDynUnionRefAdapter", [t]); const adapters = new Map(); for (const { arm, tag } of mutableArms) { @@ -684,6 +688,7 @@ export function liveDynUnionRefAdapter( `}`, ``, ); + host.journal?.built(); return sym; } @@ -732,6 +737,11 @@ export function streamTypedRefMaterializeAdapter( const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`; const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" }; host.liveDynRefAdapters.set(key, adapter); + host.journal?.allocated( + snapshot.replace(/_materialize$/, ""), + "streamTypedRefMaterializeAdapter", + [t], + ); if (isDynTypedRefType(t)) { const fields = host.classMeta.get(t.className)?.def.fields; if ( @@ -744,6 +754,7 @@ export function streamTypedRefMaterializeAdapter( ) { host.declare(`declare ptr @scr_dyn_class_view_unavailable(ptr)`); adapter.snapshot = "scr_dyn_class_view_unavailable"; + host.journal?.built(); return adapter; } } @@ -1185,6 +1196,7 @@ export function streamTypedRefMaterializeAdapter( `}`, ``, ); + host.journal?.built(); return adapter; } @@ -1469,6 +1481,7 @@ export function streamFromArrayAdapter( if (existing) return existing; const sym = `sc_sfa_${host.streamFromArrayAdapters.size}`; host.streamFromArrayAdapters.set(key, sym); + host.journal?.allocated(sym, "streamFromArrayAdapter", [t]); const B = new BlockBuilder(); const unionDef = elem.kind === "union" ? host.unionsById.get(elem.unionId) : undefined; if (elem.kind === "union" && !unionDef) { @@ -1585,5 +1598,17 @@ export function streamFromArrayAdapter( `}`, ``, ); + host.journal?.built(); return sym; } + +registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType)); +registerReplay("liveDynUnionRefAdapter", (host, args) => + liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }), +); +registerReplay("streamTypedRefMaterializeAdapter", (host, args) => + streamTypedRefMaterializeAdapter(host, args[0] as IrType).snapshot.replace(/_materialize$/, ""), +); +registerReplay("streamFromArrayAdapter", (host, args) => + streamFromArrayAdapter(host, args[0] as IrType & { kind: "array" }), +); diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 0570d0ea7..42c803697 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -4,6 +4,7 @@ import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js"; import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { unionPeekLines, unionTagLines } from "./union-repr.js"; +import { registerReplay } from "./emit-journal.js"; export function streamDataAdapter( host: LlvmEmitterContext, @@ -14,6 +15,7 @@ export function streamDataAdapter( if (sym) return sym; sym = `sc_ee_dad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamDataAdapter", [cbT]); const p = cbT.params[0]; if (cbT.params.length > 1) { throw new InternalCompilerError( @@ -207,6 +209,7 @@ export function streamDataAdapter( d.push(`}`, ``); } host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -220,6 +223,7 @@ export function streamDoneFnFor( if (sym) return sym; sym = `sc_sd_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]); const errT = doneT.params[0]; let errTag = -1; if (errT !== undefined) { @@ -347,6 +351,7 @@ export function streamDoneFnFor( d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -360,6 +365,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: if (sym) return sym; sym = `sc_fsren_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]); const param = cbT.params[0]!; const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; fs.rename callback ${typeKey(cbT)}`, @@ -415,6 +421,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind: ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -447,6 +454,7 @@ export function cryptoIntThunkFor( if (sym) return sym; sym = `sc_cryptoint_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]); const d: string[] = [ `define internal void @${sym}(ptr %cb, double %value) ${FN_ATTRS} { ; randomInt callback ${typeKey(cbT)}`, `entry:`, @@ -489,6 +497,7 @@ export function cryptoIntThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -507,6 +516,7 @@ export function zlibBytesThunkFor( if (sym) return sym; sym = `sc_${family}bytes_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]); const d: string[] = [ `define internal void @${sym}(ptr %cb, ptr %err, ptr %value) ${FN_ATTRS} { ; zlib bytes callback ${typeKey(cbT)}`, `entry:`, @@ -618,6 +628,7 @@ export function zlibBytesThunkFor( ``, ); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } @@ -633,6 +644,7 @@ export function streamCbThunkFor( if (sym) return sym; sym = `sc_scb_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); + host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]); const runtimeParams = kind === "r" ? ["ptr %s", "double %size"] @@ -901,5 +913,35 @@ export function streamCbThunkFor( } d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); + host.journal?.built(); return sym; } + +registerReplay("streamDataAdapter", (host, args) => + streamDataAdapter(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("streamDoneFnFor", (host, args) => + streamDoneFnFor( + host, + args[0] as "w" | "f" | "d" | "t" | "l", + args[1] as IrType & { kind: "func" }, + ), +); +registerReplay("fsRenameThunkFor", (host, args) => + fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("cryptoIntThunkFor", (host, args) => + cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }), +); +registerReplay("zlibBytesThunkFor", (host, args) => + zlibBytesThunkFor( + host, + args[0] as IrType & { kind: "func" }, + args[1] as boolean, + args[2] as string, + args[3] as BytesThunkValue, + ), +); +registerReplay("streamCbThunkFor", (host, args) => + streamCbThunkFor(host, args[0] as "r" | "w" | "f" | "d" | "t" | "l" | "e", args[1] as IrType), +); diff --git a/packages/compiler/src/backend/llvm/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts index ce9c40e21..4d151fc7e 100644 --- a/packages/compiler/src/backend/llvm/lib-connect.ts +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -3,6 +3,7 @@ import { typeKey } from "../../ir/ir.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; /** CONNECT transfers owned request/socket/response values to the listener. */ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): string { @@ -18,6 +19,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str if (previous !== undefined) return previous; const symbol = `sc_connect_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "connectAdapter", [cb, h2]); const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v"; const release = h2 ? "scr_http_res_release_v" : "scr_net_sock_release_v"; host.declare(`declare ptr @${retain}(ptr)`); @@ -37,6 +39,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str "}", "", ); + host.journal?.built(); return symbol; } @@ -70,3 +73,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L a[2]!, ]); } + +registerReplay("connectAdapter", (host, args) => + connectAdapter(host, args[0] as LlValue, args[1] as boolean), +); diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 184fdb99e..e32fd238d 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -11,6 +11,7 @@ import { ptr, } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; function messageAdapter(host: LlvmEmitterContext, type: IrType): string { if (type.kind !== "record") @@ -20,6 +21,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { if (previous !== undefined) return previous; const symbol = `sc_dgram_message_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "messageAdapter", [type]); const shape = host.recordsById.get(type.shapeId); if (shape === undefined) throw new InternalCompilerError("missing datagram remote info shape"); const lines: string[] = []; @@ -48,6 +50,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -67,6 +70,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { if (previous !== undefined) return previous; const symbol = `sc_dns_lookup_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "lookupAdapter", [cb]); host.declare("declare ptr @scr_error_new(i32, ptr)"); host.declare("declare ptr @scr_error_retain_v(ptr)"); host.declare("declare void @scr_error_release_v(ptr)"); @@ -93,6 +97,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -250,3 +255,6 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L throw new InternalCompilerError(`unhandled datagram call ${e.fn}`); } } + +registerReplay("messageAdapter", (host, args) => messageAdapter(host, args[0] as IrType)); +registerReplay("lookupAdapter", (host, args) => lookupAdapter(host, args[0] as LlValue)); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index 791987f86..321f1d005 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -13,6 +13,7 @@ import { requestHandler, } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; /** The runtime owns the answer closure; its arguments arrive owned from the caller. */ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { @@ -22,6 +23,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { if (previous !== undefined) return previous; const symbol = `sc_sni_answer_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "sniAnswer", [type]); const params = ["ptr %self"], body: string[] = []; let hasError = "false", @@ -82,6 +84,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { "}", "", ); + host.journal?.built(); return symbol; } @@ -318,3 +321,5 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params); return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]); } + +registerReplay("sniAnswer", (host, args) => sniAnswer(host, args[0] as IrType)); diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts index c6bc4d97b..56e94461f 100644 --- a/packages/compiler/src/backend/llvm/lib-lookup.ts +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -1,9 +1,10 @@ import { InternalCompilerError } from "../../errors.js"; -import { typeKey } from "../../ir/ir.js"; +import { type IrType, typeKey } from "../../ir/ir.js"; import { mangleRecordRelease, mangleRecordStruct } from "../mangle.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js"; import { FN_ATTRS } from "./shapes.js"; +import { registerReplay } from "./emit-journal.js"; export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const args = e.args.map((arg) => host.emitExpr(arg)), @@ -12,6 +13,18 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV answer = lookup.params[2]; if (answer?.kind !== "func" || answer.params.length !== 2) throw new InternalCompilerError("lookup answer must have two parameters"); + const symbol = lookupAnswer(host, answer); + host.usesTimers = true; + host.toHeap(cb); + return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ + ...args.map((arg) => abiValue(host, arg)), + ptr("@" + symbol), + ]); +} + +/** The thunk that hands a lookup's answer (error, addresses) to the + * runtime, one per answer signature. */ +function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func" }): string { const error = answer.params[0]!, addresses = answer.params[1]!; if (error.kind !== "union" || addresses.kind !== "array" || addresses.elem.kind !== "record") @@ -26,6 +39,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV if (symbol === undefined) { symbol = `sc_lookup_answer_${host.resolveThunks.size}`; host.resolveThunks.set(key, symbol); + host.journal?.allocated(symbol, "lookupAnswer", [answer]); for (const declaration of [ "declare ptr @scr_str_retain_v(ptr)", "declare void @scr_str_release(ptr)", @@ -84,11 +98,11 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV "}", "", ); + host.journal?.built(); } - host.usesTimers = true; - host.toHeap(cb); - return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ - ...args.map((arg) => abiValue(host, arg)), - ptr("@" + symbol), - ]); + return symbol; } + +registerReplay("lookupAnswer", (host, args) => + lookupAnswer(host, args[0] as IrType & { kind: "func" }), +); diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 2f45c3cd2..60270adda 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -31,6 +31,13 @@ import type { InlineUnionFields } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; export interface PublishHost { + /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, + * opened, used and entry. */ + journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalBuilt?: (() => void) | undefined; + journalOpened?: ((op: string) => void) | undefined; + journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined; declare(decl: string): void; /** An interned NUL-terminated constant. */ cstr(text: string): string; @@ -103,6 +110,14 @@ export class PublishWalkers { constructor(private readonly host: PublishHost) {} + /** Journal replay (emit-journal.ts): builds or finds the walker a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + if (op === "publish.walker") + return this.walker(args[0] as IrType, args[1] as string, args[2] as SrcLoc)!; + return this.refusal(args[0] as string, args[1] as string); + } + /** The definitions emitted so far (module-level LLVM text). */ defs(): string[] { return this.defsOut; @@ -119,6 +134,7 @@ export class PublishWalkers { } const fn = this.walker(t, "the published value", loc); if (fn === null) return []; + this.host.journalUsed?.(fn, "publish.walker", [t, "the published value", loc]); this.host.declare(`declare void @scr_publish(ptr, ptr)`); return [` call void @scr_publish(ptr ${v}, ptr ${fn})`]; } @@ -156,8 +172,10 @@ export class PublishWalkers { if (known !== undefined) return known; const sym = `@sc_pub_${this.counter++}`; this.walkers.set(key, sym); + this.host.journalOpened?.("publish.walker"); const body = this.body(t, sym, path, loc); this.defsOut.push(...body, ``); + this.host.journalBuilt?.(); return sym; } @@ -172,6 +190,7 @@ export class PublishWalkers { if (known !== undefined) return known; const sym = `@sc_pub_${this.counter++}`; this.refusals.set(key, sym); + this.host.journalOpened?.("publish.refusal"); this.host.declare(`declare void @scr_pub_refuse_at(ptr, ptr, ptr)`); this.defsOut.push( `define internal void ${sym}(ptr %o, ptr %ctx) ${FN_ATTRS} { ; refuse ${llvmComment(detail)}`, @@ -181,6 +200,7 @@ export class PublishWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return sym; } diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f667..6daa156ce 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -68,6 +68,13 @@ const CYC_MAX_TYPES = 32768; /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { + /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, + * opened, used and entry. */ + journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalBuilt?: (() => void) | undefined; + journalOpened?: ((op: string) => void) | undefined; + journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; + journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined; /** The hierarchy root of a class (a program class or a runtime base), * or undefined for classes outside the class graph. */ classRootOf?(className: string): string | undefined; @@ -386,6 +393,7 @@ function nullableRetainSym(host: ShapeHost, arm: IrType): string { const sym = `@sc_nretain_${arm.kind}`; if (!host.nullableUnions.retainWrappers.has(sym)) host.nullableUnions.retainWrappers.set(sym, retainSym(host, arm)); + host.journalEntry?.("retainWrapper", sym, host.nullableUnions.retainWrappers.get(sym)); return sym; } diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 46dbc86e1..044cb79ae 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -62,6 +62,21 @@ export class LlWalkers { constructor(private readonly host: WalkerHost) {} + /** Journal replay (emit-journal.ts): builds or finds the helper a chunk + * emitter recorded, and answers its name. */ + replayHelper(op: string, args: readonly unknown[]): string { + switch (op) { + case "walkers.jsonWriteHelper": + return this.jsonWriteHelper(args[0] as IrType); + case "walkers.recordFieldWriter": + return this.recordFieldWriter(args[0] as string); + case "walkers.unionJoinHelper": + return this.unionJoinHelper(args[0] as string); + default: + throw new InternalCompilerError(`llvm emitter bug: no walker replay ${op}`); + } + } + private get S(): "i32" | "i64" { return this.host.sizeType; } @@ -191,6 +206,7 @@ export class LlWalkers { if (existing) return existing; const name = `sc_jw_${this.jsonWriters.size}`; this.jsonWriters.set(key, name); + this.host.journalAllocated?.(name, "walkers.jsonWriteHelper", [t]); const B = new BlockBuilder(); switch (t.kind) { case "f64": @@ -244,6 +260,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -457,6 +474,7 @@ export class LlWalkers { throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`); const name = `sc_jwf_${this.recordFieldWriters.size}`; this.recordFieldWriters.set(shapeId, name); + this.host.journalAllocated?.(name, "walkers.recordFieldWriter", [shapeId]); const fieldBase = this.host.nullableUnions.fieldBase(shapeId); const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase])); const byName = new Map(shape.fields.map((f) => [f.name, f])); @@ -508,6 +526,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } @@ -852,6 +871,7 @@ export class LlWalkers { if (!def) throw new InternalCompilerError(`llvm emitter bug: join of unknown union ${unionId}`); const name = `sc_uj_${this.unionJoinFns.size}`; this.unionJoinFns.set(unionId, name); + this.host.journalAllocated?.(name, "walkers.unionJoinHelper", [unionId]); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); @@ -946,6 +966,7 @@ export class LlWalkers { `}`, ``, ); + this.host.journalBuilt?.(); return name; } } diff --git a/packages/compiler/src/frontend/process-fork.ts b/packages/compiler/src/frontend/process-fork.ts new file mode 100644 index 000000000..744d57aa5 --- /dev/null +++ b/packages/compiler/src/frontend/process-fork.ts @@ -0,0 +1,26 @@ +/** Process primitives for forked lowering workers (unit-journal.ts). The + * native compiler fills these in; under Node they stay unset and reachable + * emit lowers every wave in-process. */ +export interface ProcessFork { + /** fork(): 0 in the worker, the worker's pid in the parent, -1 on failure. */ + fork(): number; + /** Ends the worker without running exit handlers. */ + exit(code: number): void; + /** The worker's exit status, 128 plus a signal number, or -1. */ + wait(pid: number): number; + /** Opens the lock file checker requests from workers take. */ + openLock(path: string): boolean; + lock(): void; + unlock(): void; +} + +export const processFork: { + primitives: ProcessFork | null; + /** True in a worker: checker requests take the lock (rpc-client.ts). */ + worker: boolean; + /** A worker's checker requests and the time it waited for the lock and + * for responses (SCRIPTC_TIMING). */ + requests: number; + lockWaitMs: number; + requestMs: number; +} = { primitives: null, worker: false, requests: 0, lockWaitMs: 0, requestMs: 0 }; diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index e2c3a895b..a2335444c 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -20,12 +20,28 @@ import { openNativeCache } from "./cache.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; +import { processFork } from "../frontend/process-fork.js"; declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean; declare function compilerNativeCollectCycles(): void; +declare function compilerNativeFork(): number; +declare function compilerNativeExit(code: number): void; +declare function compilerNativeWait(pid: number): number; +declare function compilerNativeLockOpen(path: string): boolean; +declare function compilerNativeLock(): void; +declare function compilerNativeUnlock(): void; async function main(): Promise { setFrontendDisposedHook(() => compilerNativeCollectCycles()); + if (process.platform !== "win32") + processFork.primitives = { + fork: () => compilerNativeFork(), + exit: (code) => compilerNativeExit(code), + wait: (pid) => compilerNativeWait(pid), + openLock: (path) => compilerNativeLockOpen(path), + lock: () => compilerNativeLock(), + unlock: () => compilerNativeUnlock(), + }; const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json"; let compiler: NativeCompiler | null = null; let cache: ReturnType = null; diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index e6e36c08f..6b21b2230 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -7,7 +7,7 @@ import type { CompileLibraryOptions, CompileRequestOptions, } from "../compile-types.js"; -import { emitLlvmModule, lowRegionTarget } from "../backend/llvm/emitter.js"; +import { emitLlvmModule, type ForkPrimitives, lowRegionTarget } from "../backend/llvm/emitter.js"; import { inlineRcOption } from "../backend/llvm/shapes.js"; import { executableLinkFeatures } from "../backend/executable-features.js"; import type { NativeLinkFeatures } from "../backend/native-link-info-core.js"; @@ -23,6 +23,7 @@ import { trackedReadFile, } from "../frontend/input-tracker.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; +import { processFork } from "../frontend/process-fork.js"; import { prepareLibrary } from "../library/prepare.js"; import type { LibraryProfile } from "../library/library-profile.js"; import type { IrModule } from "../ir/ir.js"; @@ -297,6 +298,8 @@ export function prepareNativeExecutable( runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", objectAudit: options.sanitize === true, lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), + functionWorkers: emitWorkers(), + forkPrimitives: emitForkPrimitives(), ...inlineRcOption(options.optimization, options.sanitize === true), ...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}), ...(options.optimization === "dev" && !options.strip @@ -433,3 +436,22 @@ export function prepareNativeLibrary( timing("frontend-cache-publish"); return input; } + +/** Worker processes that emit function bodies at a time + * (SCRIPTC_EMIT_WORKERS; 0 or 1 emits in this process). */ +function emitWorkers(): number { + const value = Number(process.env["SCRIPTC_EMIT_WORKERS"] ?? "4"); + return Number.isFinite(value) && value > 1 ? Math.floor(value) : 1; +} + +/** The process primitives emission workers fork with, unless the host + * cannot fork or this process is a worker itself. */ +function emitForkPrimitives(): ForkPrimitives | undefined { + const primitives = processFork.primitives; + if (primitives === null || processFork.worker) return undefined; + return { + fork: () => primitives.fork(), + exit: (code) => primitives.exit(code), + wait: (pid) => primitives.wait(pid), + }; +} diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts new file mode 100644 index 000000000..9902569f7 --- /dev/null +++ b/packages/compiler/test/chunked-emission.test.ts @@ -0,0 +1,69 @@ +import { mkdtemp, readFile, rm } 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 type { IrModule } from "../src/ir/ir.js"; +import { emitLlvmModuleSource, type LlvmTargetOptions } from "../src/backend/llvm/emitter.js"; +import { inlineRcOption } from "../src/backend/llvm/shapes.js"; + +// Programs whose function bodies request module-level helpers and tables +// that the chunk journal (emit-journal.ts) carries: checked-dynamic +// conversions, inspection, publication walkers, live record views, stream +// adapters, regex instances, colliding literal digests, interned C strings, +// speculative loop sites, typed message listeners and readers. +const PROGRAMS = [ + "1003-json-parse-unions.ts", + "114-live-record-delete.ts", + "1200-regex-test-basics.ts", + "1631-inspect-arrays.ts", + "4598-publish-constructor-stores.ts", + "4900-console-instances.ts", + "5612-readable-from-iterables.cjs", + "5702-publish-candidate-buffers.ts", + "7321-speculative-loop-bail-sites.ts", + "7360-worker-typed-records.ts", + "node24-text-utilities-coercion.js", +]; + +const OPTIONS: Record = { + release: {}, + speed: { + targetTriple: "x86_64-unknown-linux-gnu", + pointerBits: 64, + runtimeAbiMarker: true, + lowRegion: true, + ...inlineRcOption("speed", false), + }, +}; + +const text = (out: string | readonly string[]): string => + typeof out === "string" ? out : out.join(""); + +async function lower(program: string, dir: string): Promise { + const entry = join(import.meta.dirname, "../../../tests/corpus", program); + const outPath = join(dir, `${program}.ir.json`); + 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")); + return readFile(outPath, "utf8"); +} + +test("function bodies emitted in chunks merge into the text one emitter gives", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-emission-")); + try { + for (const program of PROGRAMS) { + const ir = await lower(program, dir); + const fresh = (): IrModule => deserializeModule(ir); + for (const [name, options] of Object.entries(OPTIONS)) { + const serial = text(emitLlvmModuleSource(fresh(), options)); + for (const functionChunks of [3, 7]) { + const chunked = text(emitLlvmModuleSource(fresh(), { ...options, functionChunks })); + expect(chunked === serial, `${program}, ${name}, ${functionChunks} chunks`).toBe(true); + } + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 600_000); From eace8e1a1766f76fef29bcb1f8ef790f680ce147 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:31:36 -0500 Subject: [PATCH 034/169] Give the zod 3 npm case the long per-case timeout --- tests/harness/npm.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 2df0d87ef..795e42846 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -112,9 +112,10 @@ const KNOWN: Record = { const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); /** Per-case timeouts past the default 120 s: the undici cases compile the - * built-in undici implementation's module graph, which a sanitized build - * on a CI runner takes minutes to compile. */ -const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000 }; + * built-in undici implementation's module graph, and zod3 compiles zod 3's + * 3,700-line types module. A sanitized build of either takes minutes on a + * CI runner. */ +const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000, zod3: 600_000 }; interface RunResult { stdout: Buffer; From 7ed4597407f8aa8ab7496a4003c0fc517af71a04 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:59:49 -0500 Subject: [PATCH 035/169] Match the speculative loop site global by its numbered-site name in the bail test --- packages/compiler/test/speculative-loop-emission.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index a691d856d..a81348102 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -78,15 +78,14 @@ test("a failed check finishes its iteration outside the ordinary copy", async () test("a loop site stops entering its fast copy after repeated bails", async () => { const llvm = await emitLlvm(source); - const site = /^(@scr\.spec\.bails\.\d+) = internal global i8 0$/m.exec(llvm)?.[1]; + const site = /^(@sc_spec_bails_\d+) = internal global i8 0$/m.exec(llvm)?.[1]; expect(site).toBeDefined(); const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; - const escaped = site!.replace(/\./g, "\\."); // The entry test reads the count, a bail adds one, and a completed fast // copy clears it. expect(fn).toMatch( new RegExp( - `(%\\S+) = load atomic i8, ptr ${escaped} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, + `(%\\S+) = load atomic i8, ptr ${site} unordered, align 1\\n\\s*%\\S+ = icmp ult i8 \\1, 8`, ), ); expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`)); From a6427a53c0524d5c28e3872abe6c6d28a1b13cc3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 12:10:51 -0500 Subject: [PATCH 036/169] Find own toString assignments in the syntax tree, not the file text --- .../src/frontend/lowering/lower-calls.ts | 82 +++++++++++++++++-- .../compiler/src/frontend/lowering/lowerer.ts | 3 + .../npm/cases/js-own-tostring/main.ts | 4 +- .../node_modules/nsutil/dist/index.d.ts | 3 + .../node_modules/nsutil/dist/index.js | 14 ++++ 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7e104c2f1..252e85c5a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10977,13 +10977,83 @@ function fenceClassCopyIntoFreshObject( * that function returns, not Object.prototype.toString's string. */ function jsAssignsOwnToString(lowerer: Lowerer, receiver: ts.Expression): boolean { if (!ts.isIdentifier(receiver)) return false; - let sym = lowerer.checker.getSymbolAtLocation(receiver); - if (sym !== undefined && (sym.flags & ts.SymbolFlags.Alias) !== 0) - sym = lowerer.checker.getAliasedSymbol(sym); - const decl = sym?.valueDeclaration ?? sym?.declarations[0]; + const decl = bindingDeclaration(lowerer, receiver); if (decl === undefined) return false; const sf = ts.getSourceFileOfNode(decl); if (!isJsSourceFile(sf)) return false; - const name = ts.symbolName(sym!); - return new RegExp(`\\b${name.replace(/[$]/g, "\\$")}\\.toString\\s*=[^=]`).test(sf.text); + let targets = lowerer.ownToStringTargets.get(sf); + if (targets === undefined) { + targets = ownToStringTargets(lowerer, sf); + lowerer.ownToStringTargets.set(sf, targets); + } + return targets.has(decl); +} + +const unparenthesized = (e: ts.Expression): ts.Expression => { + let out = e; + while (ts.isParenthesizedExpression(out)) out = out.expression; + return out; +}; + +/** The declaration of the binding an identifier names, through imports. */ +function bindingDeclaration(lowerer: Lowerer, id: ts.Identifier): ts.Node | undefined { + let sym = lowerer.checker.getSymbolAtLocation(id); + if (sym !== undefined && (sym.flags & ts.SymbolFlags.Alias) !== 0) + sym = lowerer.checker.getAliasedSymbol(sym); + return sym?.valueDeclaration ?? sym?.declarations?.[0]; +} + +/** The declarations of the bindings a file's assignments `x.toString = ...` + * (or `x["toString"] = ...`) give an own `toString`. An assignment through + * the parameter of an immediately called function counts for the binding + * the call passes (`(function (ns) { ns.toString = f; })(ns || (ns = {}))`). */ +function ownToStringTargets(lowerer: Lowerer, sf: ts.SourceFile): Set { + const targets = new Set(); + const visit = (node: ts.Node): void => { + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const left = unparenthesized(node.left); + const named = + (ts.isPropertyAccessExpression(left) && left.name.text === "toString") || + (ts.isElementAccessExpression(left) && + ts.isStringLiteralLike(left.argumentExpression) && + left.argumentExpression.text === "toString"); + const target = named + ? unparenthesized( + (left as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression, + ) + : undefined; + if (target !== undefined && ts.isIdentifier(target)) { + const decl = bindingDeclaration(lowerer, target); + if (decl !== undefined) { + targets.add(decl); + const passed = iifeArgumentDeclaration(lowerer, decl); + if (passed !== undefined) targets.add(passed); + } + } + } + ts.forEachChild(node, visit); + }; + visit(sf); + return targets; +} + +/** For a parameter of an immediately called function, the declaration of + * the binding its argument names: `x`, `x || (x = {})` or `(x = {})`. */ +function iifeArgumentDeclaration(lowerer: Lowerer, param: ts.Node): ts.Node | undefined { + if (!ts.isParameter(param)) return undefined; + const fn = param.parent; + if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return undefined; + let callee: ts.Node = fn; + while (ts.isParenthesizedExpression(callee.parent)) callee = callee.parent; + const call = callee.parent; + if (!ts.isCallExpression(call) || call.expression !== callee) return undefined; + const index = fn.parameters.nodes.indexOf(param); + let arg = index < 0 ? undefined : call.arguments.nodes[index]; + if (arg === undefined) return undefined; + arg = unparenthesized(arg); + if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.BarBarToken) + arg = unparenthesized(arg.left); + if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.EqualsToken) + arg = unparenthesized(arg.left); + return ts.isIdentifier(arg) ? bindingDeclaration(lowerer, arg) : undefined; } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b02a0a843..7e5e93e51 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1918,6 +1918,9 @@ export class Lowerer { /** Identifiers by spelling, per file, for the generic bindings' use * ordering (runsAfterDeclaration). */ readonly bindingIdentifiers = new Map>(); + /** Per JavaScript file, the declarations of the bindings it assigns an + * own `toString` (jsAssignsOwnToString in lower-calls.ts). */ + readonly ownToStringTargets = new Map>(); /** Per-symbol result of the never-reassigned file scan * (bindingNeverReassigned — object-literal generic-method receivers). */ readonly neverReassignedCache = new Map(); diff --git a/tests/fixtures/npm/cases/js-own-tostring/main.ts b/tests/fixtures/npm/cases/js-own-tostring/main.ts index 279e2fd66..9793b2baa 100644 --- a/tests/fixtures/npm/cases/js-own-tostring/main.ts +++ b/tests/fixtures/npm/cases/js-own-tostring/main.ts @@ -2,11 +2,13 @@ // namespace object assigns its own `toString` returning string | undefined; // Number.parseInt with one argument; and a `then` whose handler result the // declared union types (zod's ParseStatus.mergeArray pattern). -import { check, direct, mergeLater, wholePart } from "nsutil"; +import { check, direct, mergeLater, note, show, showRadix, wholePart } from "nsutil"; console.log(String(direct("a")), String(direct({ message: "b" })), String(direct(undefined))); console.log(JSON.stringify(check(undefined)), JSON.stringify(check("m"))); console.log(wholePart(1.23), wholePart(0.35), wholePart(-4.5), wholePart(12)); +const values: unknown[] = [12, "s", true, 1.5]; +console.log(values.map((value) => show(value)).join(" "), showRadix(5, 2), note.length); async function main(): Promise { console.log(JSON.stringify(await mergeLater([Promise.resolve(1), Promise.resolve("two")], { ok: true }))); console.log(JSON.stringify(await mergeLater([Promise.resolve(3), Promise.resolve("four")], { ok: false }))); diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts index f4eea833a..18f135cad 100644 --- a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.d.ts @@ -4,3 +4,6 @@ export declare function direct(message?: errorUtil.ErrMessage): string | undefin export declare function wholePart(value: number): number; export type Merged = { status: "valid"; value: unknown[] } | { status: "aborted" }; export declare function mergeLater(items: Promise[], status: { ok: boolean }): Promise; +export declare const note: string; +export declare function show(value: unknown): string; +export declare function showRadix(value: number, radix: number): string; diff --git a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js index 272a4631c..105187f49 100644 --- a/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js +++ b/tests/fixtures/npm/cases/js-own-tostring/node_modules/nsutil/dist/index.js @@ -12,3 +12,17 @@ export function mergeLater(items, status) { return Promise.all(items).then((results) => status.ok ? { status: "valid", value: results } : INVALID); } const INVALID = Object.freeze({ status: "aborted" }); +// Only an assignment to this binding's own toString counts: not this +// comment's value.toString = (x) => x, a string, or another function's +// parameter of the same name. +export const note = "value.toString = (x) => x"; +export function show(value) { + return value.toString(); +} +export function showRadix(value, radix) { + return value.toString(radix); +} +export function patch(value) { + value.toString = () => "patched"; + return value; +} From 83d426f9be4c4d45fd3131a7d3756ba8b8e3acb3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 12:14:23 -0500 Subject: [PATCH 037/169] Support FinalizationRegistry with bookkeeping and no cleanup callbacks wasm-bindgen output constructs a FinalizationRegistry per exported class when its module loads, so every wasm-bindgen package stopped at load with SC2020. `new FinalizationRegistry(cleanup)` now creates a native registry: - register(target, held, token) and unregister(token) check their arguments and answer as Node does, with Node's TypeError messages; - the unregister tokens are keys of an internal weak map, so the registry keeps no token alive; - the cleanup callback never runs, which ECMAScript allows, so the registry keeps neither the callback nor the held values. The global answers typeof "function", instanceof, Object.prototype.toString and util.inspect as Node does. Symbol targets and tokens are refused with SC2020. `"encodeInto" in encoder` on a TextEncoder, which wasm-bindgen's loaders test at load, answered false because the encoder is a brand record without fields for its methods. `in` over such a builtin brand record now answers from the members the standard library declares, plus Object.prototype's. The limitations page says that memory a package frees only from a cleanup callback, such as the WebAssembly memory behind wasm-bindgen wrappers the program drops without free(), is not reclaimed until the program exits. Fixes #1678. --- docs/content/docs/contracts.mdx | 2 +- .../weak-collections-and-memory.mdx | 9 ++ .../compiler/src/backend/llvm/lib-dispatch.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + packages/compiler/src/contracts/check.ts | 7 +- .../lowering/expressions/identifiers.ts | 2 - .../lowering/expressions/property-access.ts | 1 + .../lowering/expressions/relational.ts | 30 ++++ .../src/frontend/lowering/lower-calls.ts | 3 + .../src/frontend/lowering/lower-classes.ts | 41 ++++- packages/compiler/src/frontend/type-mapper.ts | 2 + packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 2 + .../main.ts.json | 6 + packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_dyn_invoke.c | 7 +- packages/runtime/src/scr_json.c | 145 +++++++++++++++++- .../main.ts | 37 +++++ .../counter-wasm/counter_wasm.d.ts | 9 ++ .../node_modules/counter-wasm/counter_wasm.js | 61 ++++++++ .../node_modules/counter-wasm/package.json | 1 + .../main.ts.json | 1 + 22 files changed, 362 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json create mode 100644 tests/corpus/7009-finalization-registry-wasm-loader/main.ts create mode 100644 tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts create mode 100644 tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js create mode 100644 tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json create mode 100644 tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 5af1cad2f..268f5592b 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -360,7 +360,7 @@ A class can be both `ProgramLifetime` and `Acyclic`. `Acyclic` is still checked Instances can be published to other threads with `@scriptc/threads` like other class instances: `publish()` makes them immutable and shares them by reference, writes to a published instance throw the same `TypeError` as under Node.js, and instances that were not published stay writable and are cloned by `postMessage` and `workerData`. -Static code has no `WeakRef` or `FinalizationRegistry`, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference. +Static code has no `WeakRef`, a `FinalizationRegistry` never runs its cleanup callbacks, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference. ### Lifetime regions diff --git a/docs/content/limitations/weak-collections-and-memory.mdx b/docs/content/limitations/weak-collections-and-memory.mdx index 97ee20d7b..21c6128d9 100644 --- a/docs/content/limitations/weak-collections-and-memory.mdx +++ b/docs/content/limitations/weak-collections-and-memory.mdx @@ -3,6 +3,15 @@ Copied values, other native handles, and symbol keys are unsupported. Constructor seeds support arrays; custom iterators and detached methods are unsupported. Weak collections do not retain their keys, but values that refer back to a key can keep it alive until deletion or collection release. +`FinalizationRegistry` keeps its registrations: `register` and `unregister` check their arguments and answer as in Node.js. +Its cleanup callbacks never run, which the ECMAScript specification allows. +Program output matches Node.js, which runs cleanup callbacks only when its garbage collector decides to. +Memory that a package frees only from a cleanup callback is not reclaimed until the program exits. +For example, a wasm-bindgen package frees the WebAssembly memory behind a wrapper object from its cleanup callback, so a long-running server that drops such wrappers without calling their `free()` method keeps that memory. +Call `free()` on wrappers the program no longer needs. +Symbol targets and unregister tokens are unsupported. +`WeakRef` is unsupported. + Memory uses reference counting. Acyclic values are freed deterministically, and cycles are collected at defined collection points. Native checked objects, arrays, Sets, and captured closures participate in cycle collection. diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 24f2bf019..5e2b0217c 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -858,6 +858,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return host.emitGenericLibCall(e); case "weakMap": case "weakSet": + case "finalizationRegistry": case "ffi": case "sharedArrayBuffer": case "arrayBuffer": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..76d7b690c 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -44,6 +44,8 @@ export const LIB_FN_SYMS: Record = { "weakSet.is": "scr_weak_set_is", "weakMap.new": "scr_weak_map_new", "weakSet.new": "scr_weak_set_new", + "finalizationRegistry.new": "scr_finalization_registry_new", + "finalizationRegistry.is": "scr_finalization_registry_is", "dyn.fromEntries": "scr_dyn_from_entries", "bytes.constructor": "scr_bytes_constructor", "bytes.construct": "scr_bytes_construct", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index c80468961..fe5dc171e 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -285,9 +285,10 @@ class ContractChecker { * the lifetimes of the objects they receive). Publication to other * threads needs no check: an instance carries a reference count of its * own until publication marks it like any other object it reaches - * (classes.ts programLifetimeRc). Static code has no WeakRef or - * FinalizationRegistry; weak collections stay correct because an entry - * keyed by an object that is never freed is never observable as removed. */ + * (classes.ts programLifetimeRc). Static code has no WeakRef, and a + * FinalizationRegistry never runs its cleanup callbacks; weak collections + * stay correct because an entry keyed by an object that is never freed is + * never observable as removed. */ private checkProgramLifetime(): void { if (this.notes.classes.size === 0) return; const roots: string[] = []; diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 1f74cea42..fc432b503 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -789,8 +789,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: "ArrayBuffer constructor values have no native lowering; use new ArrayBuffer(length) for fixed-length shared storage", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", - FinalizationRegistry: - "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", eval: "runtime code evaluation cannot be compiled ahead of time", }; lowerer.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7f3910c15..6a32c0b99 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4120,6 +4120,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", + "FinalizationRegistry", ...ERROR_CONSTRUCTOR_VALUES, ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 61558f008..7ab328120 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -147,6 +147,10 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; } + if (lowerer.isStdlibGlobal(expr.right, "FinalizationRegistry")) { + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { kind: "libCall", fn: "finalizationRegistry.is", args: [value], type: BOOL, loc }; + } const rhsBuiltin = ts.isIdentifier(expr.right) ? lowerer.builtinImportOf(expr.right) : ts.isPropertyAccessExpression(expr.right) @@ -1493,6 +1497,32 @@ export function lowerInExpression( ); if (helper) recv = { kind: "call", callee: helper, args: [recv], type: siteType, loc }; } + // A builtin class held as a brand record (TextEncoder, TextDecoder: only + // `%`-prefixed internal fields): its members are the class's prototype + // members, which the standard library declares, so `"encodeInto" in + // encoder` answers from the checker's type, plus Object.prototype's keys. + // The receiver evaluates for its effects. + if (recv.type.kind === "record") { + const shape = lowerer.shapes.get(recv.type.shapeId); + if (shape && shape.fields.length > 0 && shape.fields.every((f) => f.name.startsWith("%"))) { + const declared = + lowerer.checker.getPropertyOfType(lowerer.typeOf(expr.right), key) !== undefined; + const answer: IrExpr = { + kind: "boolLit", + value: declared || OBJECT_PROTOTYPE_KEYS.has(key), + type: BOOL, + loc, + }; + if (recv.kind === "varRef") return answer; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: answer, + type: BOOL, + loc, + }; + } + } // Error-rooted receivers (builtin or user subclass — the isErrnoException // predicate's `err instanceof Error && "code" in err` shape): `code` // answers from the runtime error's code slot (stamped by fs/system/ diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..9378ccbb6 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4318,6 +4318,9 @@ const DYN_DISPATCH_METHODS = new Set([ "additionalHeaders", "altsvc", "origin", + // FinalizationRegistry's methods (scr_finalization_registry_new). + "register", + "unregister", // Worker and MessagePort methods use the owning context's handle adapter. "postMessage", "terminate", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..307b3d508 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -10394,6 +10394,45 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }; } + // A FinalizationRegistry construction: register and unregister keep + // their bookkeeping, and the cleanup callback never runs, which + // ECMAScript allows (scr_finalization_registry_new). + if ( + symbol !== null && + ts.symbolName(symbol) === "FinalizationRegistry" && + lowerer.isStdlibSymbol(symbol) + ) { + const args = expr.arguments?.nodes ?? []; + if (args.some(ts.isSpreadElement)) { + lowerer.unsupported( + "SC1090", + expr, + "FinalizationRegistry construction with spread arguments", + ); + } + const cleanup = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + const surplus = args.slice(1).map((arg) => lowerer.lowerExpr(arg)); + const created: IrExpr = { + kind: "libCall", + fn: "finalizationRegistry.new", + args: [cleanup], + type: DYN, + loc, + }; + if (surplus.length === 0) return created; + // Surplus arguments evaluate after the cleanup argument and are ignored. + const local = lowerer.declareHiddenLocal("%finalizationCleanup", DYN); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: local.id, init: cleanup, loc }, + ...surplus.map((value): IrStmt => ({ kind: "exprStmt", expr: value, loc })), + ], + result: { ...created, args: [varRef(local.id, DYN, loc)] }, + type: DYN, + loc, + }; + } // `new Set()`: construct + bulk add from supported values, // including live collection cursors. Duplicates collapse in insertion order. if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) { @@ -10605,8 +10644,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { Boolean: "boxed wrapper objects have no lowering — use the boolean primitive", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", - FinalizationRegistry: - "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", ArrayBuffer: diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index bd2a32c37..25ecac04a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3155,6 +3155,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Native WeakMap keeps reference identities in checked-dynamic storage. // The runtime rejects reference kinds without a weak lifetime contract. if (isStdlibInterface("WeakMap") || isStdlibInterface("WeakSet")) return DYN; + // FinalizationRegistry is a native handle with register and unregister. + if (isStdlibInterface("FinalizationRegistry")) return DYN; // Set: Map's sibling — same provenance rule and key domain. // Anything else stays unmapped; // the `new Set` lowering names the offending element type specifically. diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..b44d73622 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -88,6 +88,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "stream.unpipeDyn", "weakMap.new", "weakSet.new", + // Node's "cleanup must be callable" TypeError. + "finalizationRegistry.new", "dyn.fromEntries", "bytes.constructor", "bytes.instanceOf", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..dd1aec3aa 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3591,6 +3591,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "weakSet.is": { argTypes: [DYN], result: BOOL }, "weakMap.new": { argTypes: [DYN], result: DYN }, "weakSet.new": { argTypes: [DYN], result: DYN }, + "finalizationRegistry.new": { argTypes: [DYN], result: DYN }, + "finalizationRegistry.is": { argTypes: [DYN], result: BOOL }, "dyn.fromEntries": { argTypes: [DYN], result: DYN }, "bytes.constructor": { argTypes: [STRING], result: DYN }, "bytes.instanceOf": { argTypes: [DYN, DYN], result: BOOL }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json new file mode 100644 index 000000000..89ee292b3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7009-finalization-registry-wasm-loader/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7009-finalization-registry-wasm-loader/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bb3b06847..97db18618 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -169,6 +169,7 @@ typedef enum { X(FETCH_RESPONSE, 12) /* native static-fetch Response */ \ X(FILE, 42) /* ScrBlob with a name — new File() */ \ X(FILE_HANDLE, 17) /* fs/promises.FileHandle */ \ + X(FINALIZATION_REGISTRY, 72) /* FinalizationRegistry (bookkeeping only) */ \ X(GLOBAL, 22) /* native global object identity */ \ X(H2_SESSION, 4) /* ScrH2Session — Http2Session (client & server) */ \ X(H2_STREAM, 5) /* ScrH2Stream — Http2Stream (client & server) */ \ @@ -1066,6 +1067,8 @@ ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry); bool scr_weak_map_is(const ScrDyn *value); bool scr_weak_set_is(const ScrDyn *value); ScrDyn *scr_weak_map_new(ScrDyn *entries); +ScrDyn *scr_finalization_registry_new(ScrDyn *cleanup); +bool scr_finalization_registry_is(const ScrDyn *value); ScrDyn *scr_weak_set_new(ScrDyn *values); ScrDyn *scr_dyn_from_entries(ScrDyn *entries); ScrDyn *scr_dyn_mark_snapshot(ScrDyn *value); /* consumes and returns +1 */ diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 80e01b08c..bfe4fc9f7 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1854,9 +1854,10 @@ static const char dyn_s29[] SCR_DYN_COMPAT_STR = "Date"; static const char dyn_s30[] SCR_DYN_COMPAT_STR = "Map"; static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set"; static const char dyn_s32[] SCR_DYN_COMPAT_STR = ""; +static const char dyn_s33[] SCR_DYN_COMPAT_STR = "FinalizationRegistry"; static const char *const dyn_global_ctor_names[] = { - dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, + dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, dyn_s33, }; #define DYN_GLOBAL_CTORS (sizeof dyn_global_ctor_names / sizeof dyn_global_ctor_names[0]) static SCR_TL ScrDyn *dyn_global_ctors[DYN_GLOBAL_CTORS]; @@ -1896,8 +1897,8 @@ static ScrDyn *dyn_global_ctor(const char *name) { } if (!dyn_global_ctors[i]) { /* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1, - * the collections 0. */ - uint32_t length = i == 0 || i == 5 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0; + * the collections 0, FinalizationRegistry 1. */ + uint32_t length = i == 0 || i == 5 || i == 8 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0; dyn_global_ctors[i] = scr_dyn_new_func(scr_closure_new((void *)dyn_global_ctor_names[i], 0), dyn_global_ctor_call, length, dyn_s6, dyn_global_ctor_names[i]); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 1c85a928a..44416efc6 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3964,6 +3964,7 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD static const struct { const char *name; int tag; } kinds[] = { {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, + {"FinalizationRegistry", SCR_DYNH_FINALIZATION_REGISTRY}, }; if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { @@ -7030,6 +7031,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET || + d->v.handle.tag == SCR_DYNH_FINALIZATION_REGISTRY || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_BLOB || d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) { @@ -14417,7 +14419,8 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { *kind = key->v.fn.class_obj ? 3 : 2; return 1; case SCR_DYN_HANDLE: if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_SHARED_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP && - key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO && + key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_FINALIZATION_REGISTRY && + key->v.handle.tag != SCR_DYNH_STDIO && key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE && key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET && key->v.handle.tag != SCR_DYNH_NET_SOCKET && key->v.handle.tag != SCR_DYNH_NET_SERVER) return -1; @@ -14596,6 +14599,146 @@ bool scr_weak_set_is(const ScrDyn *value) { ScrDyn *scr_weak_map_new(ScrDyn *seed) { return scr_weak_collection_new(seed, false); } ScrDyn *scr_weak_set_new(ScrDyn *seed) { return scr_weak_collection_new(seed, true); } +/* FinalizationRegistry. register() and unregister() validate and keep their + * bookkeeping as Node does; the cleanup callback never runs, which + * ECMAScript allows (an implementation need not collect a target or call + * its cleanup), so the registry keeps neither the callback nor the held + * values. The unregister tokens are keys of an internal WeakMap counting + * each token's registrations, so the registry keeps no token alive. */ +typedef struct { + size_t rc; + ScrDyn *tokens; /* WeakMap: token -> registration count */ +} ScrFinalizationRegistry; + +static void *scr_finalization_registry_retain(void *ptr) { + ((ScrFinalizationRegistry *)ptr)->rc++; + return ptr; +} + +static void scr_finalization_registry_release(void *ptr) { + ScrFinalizationRegistry *registry = ptr; + if (--registry->rc) return; + /* Weak collections keyed by the registry drop their entries. */ + scr_weak_dispose(registry); + scr_dyn_release(registry->tokens); + free(registry); +} + +static void scr_finalization_type_error(const char *message) { + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); +} + +/* The weak-key identity of a registration target or token, or false with + * the TypeError (or the SC2020 refusal) pending. */ +static bool scr_finalization_key(const ScrDyn *value, void **ptr, unsigned *kind, + const char *invalid) { + if (value->kind == SCR_DYN_SYMBOL) { + static const char message[] = "FinalizationRegistry with symbol targets or tokens is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + int supported = scr_weak_key(value, ptr, kind); + if (supported < 0) { + static const char message[] = "FinalizationRegistry targets of this native reference type have no weak lifetime lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + if (!supported) { + if (invalid) { + scr_finalization_type_error(invalid); + } else { + /* Node's unregister message quotes the token as String() renders it. */ + ScrStr *text = scr_dyn_string_coerce(value); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Invalid unregisterToken ('"); + if (text) scr_jb_write(&b, text->data, text->len); + scr_jb_puts(&b, "')"); + scr_str_release(text); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + } + return false; + } + return true; +} + +static ScrDyn *scr_finalization_registry_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)self; + ScrFinalizationRegistry *registry = ptr; + ScrWeakMap *tokens = registry->tokens->v.handle.ptr; + ScrDyn *first = argc > 0 ? args[0] : scr_dyn_undefined(); + void *identity = NULL; + unsigned kind = 0; + if (strcmp(method, "register") == 0) { + ScrDyn *held = argc > 1 ? args[1] : scr_dyn_undefined(); + ScrDyn *token = argc > 2 ? args[2] : scr_dyn_undefined(); + if (!scr_finalization_key(first, &identity, &kind, "FinalizationRegistry.prototype.register: invalid target")) + return NULL; + if (scr_dyn_strict_eq(first, held)) { + scr_finalization_type_error("FinalizationRegistry.prototype.register: target and holdings must not be same"); + return NULL; + } + if (token->kind != SCR_DYN_UNDEF) { + if (!scr_finalization_key(token, &identity, &kind, NULL)) return NULL; + ScrWeakEntry *entry = scr_weak_find(tokens, identity, kind); + double count = entry && entry->value->kind == SCR_DYN_NUM ? entry->value->v.num : 0; + ScrDyn *next = scr_dyn_new_num(count + 1); + bool ok = scr_weak_set(tokens, token, next); + scr_dyn_release(next); + if (!ok) return NULL; + } + return scr_dyn_retain(scr_dyn_undefined()); + } + if (strcmp(method, "unregister") == 0) { + if (!scr_finalization_key(first, &identity, &kind, NULL)) return NULL; + ScrWeakEntry *entry = scr_weak_find(tokens, identity, kind); + if (entry) scr_weak_remove(entry); + return scr_dyn_new_bool(entry != NULL); + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what ? what : method); + scr_jb_puts(&b, " is not a function"); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + return NULL; +} + +static ScrDyn *scr_finalization_registry_view(void *ptr) { + (void)ptr; + return scr_dyn_new_obj(); +} + +ScrDyn *scr_finalization_registry_new(ScrDyn *cleanup) { + if (!scr_dyn_is_callable(cleanup)) { + scr_finalization_type_error("FinalizationRegistry: cleanup must be callable"); + return NULL; + } + static const ScrDynHandleOps ops = { + .cls = "FinalizationRegistry", + .retain = &scr_finalization_registry_retain, + .release = &scr_finalization_registry_release, + .invoke = &scr_finalization_registry_invoke, + .inspect_view = &scr_finalization_registry_view, + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_FINALIZATION_REGISTRY, &ops); + ScrFinalizationRegistry *registry = calloc(1, sizeof *registry); + if (!registry) scr_json_oom(); + registry->tokens = scr_weak_collection_new(scr_dyn_undefined(), false); + if (!registry->tokens) { free(registry); return NULL; } + ScrDyn *result = scr_dyn_new_handle(registry, SCR_DYNH_FINALIZATION_REGISTRY); + return result; +} + +bool scr_finalization_registry_is(const ScrDyn *value) { + return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_FINALIZATION_REGISTRY; +} + ScrDyn *scr_dyn_from_entries(ScrDyn *seed) { if (seed->kind == SCR_DYN_UNDEF || seed->kind == SCR_DYN_NULL) { static const char message[] = "undefined is not iterable"; diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/main.ts b/tests/corpus/7009-finalization-registry-wasm-loader/main.ts new file mode 100644 index 000000000..d792f72d4 --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/main.ts @@ -0,0 +1,37 @@ +// A package loaded the way wasm-bindgen's web target loads: FinalizationRegistry +// guards and registrations run at load and in constructors, and free() +// unregisters. Registrations keep their bookkeeping; cleanup +// callbacks run at times only the garbage collector decides, so this program +// prints nothing from them, as under Node. +import { Counter, initSync, released_total } from "counter-wasm"; + +// (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)) +const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, + 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, + 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b, +]); +initSync(bytes); + +const a = new Counter(10); +a.increment(5); +const b = new Counter(2); +b.increment(1); +console.log(a.value, b.value); +b.free(); +a.free(); +console.log(a.value, released_total()); + +const registry = new FinalizationRegistry((held) => console.log("cleanup", held)); +const token = {}; +registry.register({}, "first", token); +registry.register({ id: 2 }, "second", token); +registry.register({ id: 3 }, "third"); +console.log(registry.unregister(token), registry.unregister(token), registry.unregister({})); +try { + new FinalizationRegistry(() => {}).register(token, token); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} +console.log(typeof FinalizationRegistry, registry instanceof FinalizationRegistry); +console.log(Object.prototype.toString.call(registry), registry); diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts new file mode 100644 index 000000000..525dec86f --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.d.ts @@ -0,0 +1,9 @@ +export declare class Counter { + constructor(start: number); + free(): void; + increment(by: number): number; + readonly value: number; + [Symbol.dispose](): void; +} +export declare function released_total(): number; +export declare function initSync(bytes: Uint8Array): unknown; diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js new file mode 100644 index 000000000..92715f743 --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/counter_wasm.js @@ -0,0 +1,61 @@ +// A loader in the shape wasm-bindgen's web target generates: the module's +// wrapper classes register with a FinalizationRegistry when they are +// created and unregister when free() releases them, and a TextEncoder +// polyfill guard runs at load. +let wasm; +const cachedTextEncoder = new TextEncoder(); +if (!("encodeInto" in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { read: arg.length, written: buf.length }; + }; +} +let released = 0; +const CounterFinalization = + typeof FinalizationRegistry === "undefined" + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry((ptr) => wasm.add(ptr >>> 0, 1)); + +export class Counter { + static __wrap(ptr) { + const obj = Object.create(Counter.prototype); + obj.__wbg_ptr = ptr; + CounterFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + constructor(start) { + this.__wbg_ptr = wasm.add(start, 0); + CounterFinalization.register(this, this.__wbg_ptr, this); + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + CounterFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + released += wasm.add(ptr, 0); + } + increment(by) { + this.__wbg_ptr = wasm.add(this.__wbg_ptr, by); + return this.__wbg_ptr; + } + get value() { + return this.__wbg_ptr; + } +} +if (Symbol.dispose) Counter.prototype[Symbol.dispose] = Counter.prototype.free; + +export function released_total() { + return released; +} + +export function initSync(bytes) { + if (wasm !== undefined) return wasm; + const module = new WebAssembly.Module(bytes); + const instance = new WebAssembly.Instance(module, {}); + wasm = instance.exports; + return wasm; +} diff --git a/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json new file mode 100644 index 000000000..d86e34137 --- /dev/null +++ b/tests/corpus/7009-finalization-registry-wasm-loader/node_modules/counter-wasm/package.json @@ -0,0 +1 @@ +{ "name": "counter-wasm", "version": "1.0.0", "type": "module", "main": "./counter_wasm.js", "types": "./counter_wasm.d.ts" } diff --git a/tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json b/tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json new file mode 100644 index 000000000..db9fc4168 --- /dev/null +++ b/tests/harness/corpus-costs/7009-finalization-registry-wasm-loader/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4351, "san": 2333 } From 317ec98fa5669cbd5ebd4ed8bb5f3512195d55b7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 12:31:21 -0500 Subject: [PATCH 038/169] Close async generators on early for await exits and iterate untyped native streams --- packages/cli/test/native-link-info.test.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 5 + .../compiler/src/backend/llvm/lib-dispatch.ts | 6 +- .../compiler/src/backend/native/executable.ts | 5 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../lowering/lower-async-iteration.ts | 4 +- .../src/frontend/lowering/lower-calls.ts | 15 +++ .../src/frontend/lowering/lower-generators.ts | 72 +++++++------ .../src/frontend/lowering/lower-stream.ts | 71 ++++++++----- .../test/native-codegen-integration.test.ts | 4 +- .../tests/corpus/7420-for-await-close.ts.json | 6 ++ .../corpus/7421-for-await-close.mjs.json | 6 ++ .../7422-for-await-untyped-streams.mjs.json | 6 ++ .../7423-stream-untyped-bindings.mjs.json | 6 ++ .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_dyn_for_await.c | 5 + .../runtime/src/scr_dyn_for_await_stream.c | 61 +++++++++++ packages/runtime/src/scr_stream.c | 39 ++++++- packages/runtime/src/scr_stream.h | 8 ++ tests/corpus/7420-for-await-close.ts | 100 ++++++++++++++++++ tests/corpus/7421-for-await-close.mjs | 53 ++++++++++ .../corpus/7422-for-await-untyped-streams.mjs | 38 +++++++ tests/corpus/7423-stream-untyped-bindings.mjs | 33 ++++++ .../corpus-costs/7420-for-await-close.ts.json | 1 + .../7421-for-await-close.mjs.json | 1 + .../7422-for-await-untyped-streams.mjs.json | 1 + .../7423-stream-untyped-bindings.mjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 35 files changed, 496 insertions(+), 75 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json create mode 100644 packages/runtime/src/scr_dyn_for_await_stream.c create mode 100644 tests/corpus/7420-for-await-close.ts create mode 100644 tests/corpus/7421-for-await-close.mjs create mode 100644 tests/corpus/7422-for-await-untyped-streams.mjs create mode 100644 tests/corpus/7423-stream-untyped-bindings.mjs create mode 100644 tests/harness/corpus-costs/7420-for-await-close.ts.json create mode 100644 tests/harness/corpus-costs/7421-for-await-close.mjs.json create mode 100644 tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json create mode 100644 tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a1b673630..484b03e8b 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v28"); + expect(error?.stderr).toContain("scr_runtime_abi_v29"); }); }); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index be9423442..9f21791d6 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -972,6 +972,11 @@ export class LlEmitter { get usesDynBuiltinStream(): boolean { return this.runtimeFeatures.dynBuiltinStream; } + /** A checked-dynamic for-await may hold a native stream: the program + * links the stream unit (and the dyn-async one the loop needs). */ + get dynForAwaitStreams(): boolean { + return this.runtimeFeatures.stream && this.runtimeFeatures.dynAsync; + } /** Runtime-arm dispatchers for live union values. */ readonly liveDynUnionRefAdapters = new Map(); readonly dynPromiseAdapters = new Map(); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index fd5b5d681..10d83057f 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -553,7 +553,11 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // which also takes those streams. e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream ? "scr_dyn_builtin_stream_unbox" - : LIB_FN_SYMS[e.fn]); + : // for await over a checked-dynamic value iterates a native + // stream behind it (scr_dyn_for_await_stream.c) + e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams + ? "scr_stream_dyn_async_iterator_open" + : LIB_FN_SYMS[e.fn]); if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 714bc09a5..ad318e623 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -281,6 +281,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"), // for await over checked-dynamic values. unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"), + // for await over checked-dynamic values that may hold native streams. + unit( + (u) => u.opts.dynForAwait === true && u.opts.stream && u.opts.dynAsync, + "scr_dyn_for_await_stream.c", + ), // FileHandle streams reference both the stream unit and FileHandle. unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"), // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD, diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index a616dbec2..948fa6359 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 28 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const; +export const RUNTIME_ABI_VERSION = 29 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v29" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index c2536ede8..a97769328 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 33b1ca38e..85038d8c5 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 5b427a750..95916ee9a 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -359,13 +359,15 @@ function boolRef(localId: string, loc: ReturnType): IrExpr { /** The loop's stream: a JavaScript value typed as a stream can arrive as a * checked-dynamic value (returned through an untyped function), which - * checks back into the stream class. */ + * checks back into the stream class, or as a union with it (an element + * read), which narrows to it. */ function streamSource( lowerer: Lowerer, plan: NodeReadablePlan, loc: ReturnType, ): IrExpr { const v = lowerer.lowerExpr(plan.source); + if (v.type.kind === "union") return lowerer.coerceToExpected(v, plan.type); if (v.type.kind !== "dyn") return v; return { kind: "dynCheck", value: v, type: plan.type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 875fb9e7a..0ffaab400 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4338,6 +4338,21 @@ export function lowerDynDispatchMethodCall( lowerer.chainBlocked(call, access) ) return null; + // readable.compose lowers its stages from their syntax, so it needs the + // stream's class: a stream a JavaScript function returns untyped has none. + if (method === "compose") { + const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + if ( + recvT?.kind === "object" && + streamSidesOf(lowerer, lowerer.classes.get(recvT.className)) !== null + ) { + lowerer.noLowering( + "readable.compose on a stream held as a checked-dynamic value", + call, + "type the stream where it comes from, such as a JSDoc @returns {Readable} on the function that returns it", + ); + } + } if (call.arguments.nodes.some((arg) => ts.isSpreadElement(arg))) { const loc = locOf(call); const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN); diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index b4e3ac102..80463fa6c 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -687,8 +687,12 @@ export function lowerForAwaitGenerator( lowerer.scopes.push(new Map()); try { const g = lowerer.declareHiddenLocal("%fag", genT); - const done = lowerer.declareHiddenLocal("%fagdone", BOOL); - done.mutable = true; + // Whether a value has arrived and the body holds the generator open: + // leaving the loop then (break, return, a throw, or a return() at a + // yield in the body) closes it, as AsyncIteratorClose does; an + // exhausted generator and a rejected next() are not closed. + const active = lowerer.declareHiddenLocal("%fagactive", BOOL); + active.mutable = true; const p = lowerer.declareHiddenLocal("%fagpromise", promiseT); const r = lowerer.declareHiddenLocal("%fagresult", recT); const gRef = (): IrExpr => ({ kind: "varRef", localId: g.id, type: genT, loc }); @@ -709,7 +713,14 @@ export function lowerForAwaitGenerator( `for-await over an async generator yielding '${lowerer.fmt(genT.yieldT)}' (no per-element extraction exists)`, ); } + const setActive = (value: boolean): IrStmt => ({ + kind: "assign", + localId: active.id, + value: { kind: "boolLit", value, type: BOOL, loc }, + loc, + }); const head: IrStmt[] = [ + setActive(false), { kind: "varDecl", localId: p.id, @@ -737,19 +748,12 @@ export function lowerForAwaitGenerator( type: BOOL, loc, }, - then: [ - { - kind: "assign", - localId: done.id, - value: { kind: "boolLit", value: true, type: BOOL, loc }, - loc, - }, - { kind: "break", loc }, - ], + then: [{ kind: "break", loc }], else_: null, loc, }, ...generatorLoopBinding(lowerer, stmt, extracted, genT.yieldT), + setActive(true), ]; const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); const closePromise: IrExpr = { @@ -766,34 +770,40 @@ export function lowerForAwaitGenerator( { kind: "varDecl", localId: g.id, init: iterable, loc }, { kind: "varDecl", - localId: done.id, + localId: active.id, init: { kind: "boolLit", value: false, type: BOOL, loc }, loc, }, { - kind: "while", - cond: { kind: "boolLit", value: true, type: BOOL, loc }, - body: [...head, ...body], - ...(labels && { labels }), - loc, - }, - { - kind: "if", - cond: { - kind: "unary", - op: "!", - operand: { kind: "varRef", localId: done.id, type: BOOL, loc }, - type: BOOL, - loc, - }, - then: [ + kind: "tryCatch", + tryBody: [ { - kind: "exprStmt", - expr: { kind: "awaitExpr", value: closePromise, type: recT, loc }, + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [...head, ...body], + ...(labels && { labels }), + loc, + }, + ], + catchBody: null, + catchLocalId: null, + // After a throw, an error from closing the generator is dropped. + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: { kind: "varRef", localId: active.id, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { kind: "awaitExpr", value: closePromise, type: recT, loc }, + loc, + }, + ], + else_: null, loc, }, ], - else_: null, loc, }, ], diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 214a2402e..6aa8aef96 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -3944,6 +3944,29 @@ function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], lo /* ── the Readable operators ──────────────────────────────────────────── */ /** The Readable operators (scr_stream_ops.c — Node's operators.js). */ +/** A stream member's receiver, lowered: a value the checker types as a + * stream class whose lowering is checked-dynamic (a JavaScript binding + * that holds an untyped call's result) or a union (an element read) + * converts to that class, checked where the member runs. */ +function streamReceiverExpr(lowerer: Lowerer, node: ts.Expression): IrExpr { + const value = lowerer.lowerExpr(node); + if (value.type.kind !== "dyn" && value.type.kind !== "union") return value; + let classT: IrType | null; + try { + classT = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + return value; + } + if ( + classT?.kind !== "object" || + streamSidesOf(lowerer, lowerer.classes.get(classT.className)) === null + ) + return value; + return value.type.kind === "dyn" + ? { kind: "dynCheck", value, type: classT, loc: value.loc } + : lowerer.coerceToExpected(value, classT); +} + export const STREAM_OPERATORS: ReadonlySet = new Set([ "map", "filter", @@ -4066,7 +4089,7 @@ function lowerStreamOperator( ): IrExpr { const loc = locOf(call); const args = call.arguments.nodes; - const recv = lowerer.lowerExpr(access.expression); + const recv = streamReceiverExpr(lowerer, access.expression); const readable: IrType = { kind: "object", className: "%Readable" }; const opts = (i: number): IrExpr => lowerOperatorValue(lowerer, args[i], `${member} options`, loc, true); @@ -4210,7 +4233,7 @@ function pipeIntoStdio( ): IrExpr { const loc = locOf(call); // JS order: the receiver, then the destination, then the options. - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type); const options = call.arguments.nodes[1]; @@ -4332,7 +4355,7 @@ function pipeIntoDyn( ): IrExpr { const loc = locOf(call); noteUntypedStreamFlow(lowerer, null); - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type); const stmts: IrStmt[] = [ @@ -4621,7 +4644,7 @@ export function lowerStreamMethodCall( loc, ); } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", @@ -4637,7 +4660,7 @@ export function lowerStreamMethodCall( if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") { // unshift(value): an object-mode stream's value at the front, null // the end of the stream, as Node's readableAddChunk takes them - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", @@ -4647,7 +4670,7 @@ export function lowerStreamMethodCall( loc, }; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (kind === "null") { return { kind: "libCall", fn: "readable.pushNull", args: [receiver], type: BOOL, loc }; } @@ -4700,7 +4723,7 @@ export function lowerStreamMethodCall( if (member === "read") { requireSide(readableSide, "read"); if (args.length > 1) lowerer.noLowering(`read with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const size = args[0] ? lowerer.lowerExprExpecting(args[0], F64) : numLit(-1, loc); const type: IrType = unionOf(lowerer, [BYTES, { kind: "nullT" }]); const read: IrExpr = { @@ -4716,7 +4739,7 @@ export function lowerStreamMethodCall( if (member === "pause" || member === "resume") { requireSide(readableSide, member); if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: member === "pause" ? "readable.pause" : "readable.resume", @@ -4729,7 +4752,7 @@ export function lowerStreamMethodCall( if (member === "isPaused") { requireSide(readableSide, "isPaused"); if (args.length !== 0) lowerer.noLowering(`isPaused with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: "readable.isPaused", args: [receiver], type: BOOL, loc }; } @@ -4746,7 +4769,7 @@ export function lowerStreamMethodCall( // Transform, and pipe() answers the value itself (the receiver // evaluates first, as in JS). if (isValueStream(lowerer, args[0]!)) { - const receiverValue = lowerer.lowerExpr(access.expression); + const receiverValue = streamReceiverExpr(lowerer, access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const hashValue = lowerer.lowerExpr(args[0]!); const hash = lowerer.declareHiddenLocal("%pipeHash", hashValue.type); @@ -4796,7 +4819,7 @@ export function lowerStreamMethodCall( ); } const end = pipeEndOption(lowerer, args[1], loc); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: generalPipe(lowerer, dst.type), @@ -4809,7 +4832,7 @@ export function lowerStreamMethodCall( if (member === "unpipe") { requireSide(readableSide, "unpipe"); if (args.length > 1) lowerer.noLowering(`unpipe with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (args[0] !== undefined) { const dst = lowerer.lowerExpr(args[0]); if (dst.type.kind === "dyn") return unpipeDyn(lowerer, receiver, dst, loc); @@ -4870,7 +4893,7 @@ export function lowerStreamMethodCall( if (member === "destroy") { if (args.length > 1) lowerer.noLowering(`destroy with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); if (args[0] !== undefined) { const err = lowerer.lowerExpr(args[0]); const rootsAtError = (t: IrType): boolean => { @@ -4923,7 +4946,7 @@ export function lowerStreamMethodCall( // the callback took its place (a byte-mode side encodes a string // chunk with it). const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); const tail: IrExpr[] = []; if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN)); @@ -4953,7 +4976,7 @@ export function lowerStreamMethodCall( } return writeEnc(args[1], undefined); } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc }; } @@ -4963,7 +4986,7 @@ export function lowerStreamMethodCall( // lowered value may have collapsed to one arm (checker narrowing) — // re-dispatch on its IR type. if (args.length > 1) lowerer.noLowering("write with a union chunk and more arguments", call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExpr(args[0]!); const fn = chunk.type.kind === "union" @@ -4986,7 +5009,7 @@ export function lowerStreamMethodCall( return writeEnc(args[1], undefined); cbArg = args[1]; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = kind === "bytes" ? lowerer.lowerExprExpecting(args[0]!, BYTES) @@ -5021,7 +5044,7 @@ export function lowerStreamMethodCall( encNode = args[1]; cbArg = args[2]; } - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); let flags = 0; const tail: IrExpr[] = []; if (chunkNode !== undefined) { @@ -5068,7 +5091,7 @@ export function lowerStreamMethodCall( if (member === "cork" || member === "uncork") { requireSide(writableSide, member); if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: member === "cork" ? "writable.cork" : "writable.uncork", @@ -5088,7 +5111,7 @@ export function lowerStreamMethodCall( ); } const enc = bufEncoding(lowerer, "setEncoding", args[0]!); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); return { kind: "libCall", fn: "readable.setEncoding", @@ -5104,7 +5127,7 @@ export function lowerStreamMethodCall( requireSide(writableSide, "setDefaultEncoding"); if (args.length > 1) lowerer.noLowering(`setDefaultEncoding with ${args.length} arguments`, call); - const receiver = lowerer.lowerExpr(access.expression); + const receiver = streamReceiverExpr(lowerer, access.expression); const enc = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); return { kind: "libCall", @@ -5257,7 +5280,7 @@ export function lowerStreamProperty( const propCall = (type: IrType): IrExpr => ({ kind: "libCall", fn: "stream.prop", - args: [lowerer.lowerExpr(expr.expression), strLit(name, loc)], + args: [streamReceiverExpr(lowerer, expr.expression), strLit(name, loc)], type, loc, }); @@ -5280,7 +5303,7 @@ export function lowerStreamProperty( const read: IrExpr = { kind: "libCall", fn: "readable.flowing", - args: [lowerer.lowerExpr(expr.expression)], + args: [streamReceiverExpr(lowerer, expr.expression)], type, loc, }; @@ -5294,7 +5317,7 @@ export function lowerStreamProperty( const read: IrExpr = { kind: "libCall", fn: "stream.errored", - args: [lowerer.lowerExpr(expr.expression)], + args: [streamReceiverExpr(lowerer, expr.expression)], type, loc, }; diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 71b9570b4..bb6ccf874 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v29"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v28"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v29"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json new file mode 100644 index 000000000..6e48bbc1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7420-for-await-close.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7420-for-await-close.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json new file mode 100644 index 000000000..dea911d1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7421-for-await-close.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7421-for-await-close.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json new file mode 100644 index 000000000..c2677d0b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7422-for-await-untyped-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7422-for-await-untyped-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json new file mode 100644 index 000000000..180721bd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7423-stream-untyped-bindings.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7423-stream-untyped-bindings.mjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 3d22b2360..71126f806 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 322832f9f..1c0458d7e 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -129,6 +129,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_dyn_handle_props.c", "handleProps"], // for await over checked-dynamic values. ["scr_dyn_for_await.c", "dynForAwait"], + ["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")], // fs.ReadStream / fs.WriteStream reference only the stream unit and base units. ["scr_fs_stream.c", "stream"], // FileHandle streams reference both the stream unit and FileHandle. diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 909412bc7..b0ec0005b 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 9bd5abf8c..317ee124b 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v28(void) {} +void scr_runtime_abi_v29(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 35332d0ca..6f3aae93a 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v28(void); +void scr_runtime_abi_v29(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bb3b06847..92bac77ae 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -839,6 +839,7 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell); /* for await over a checked-dynamic value (scr_dyn_for_await.c). */ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell); bool scr_dyn_async_iterator_was_sync(void); +void scr_dyn_async_iterator_set_sync(bool sync); ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value); /* from.js's probe: 2 when value[Symbol.asyncIterator] is set, 1 when * value[Symbol.iterator] is, 0 for neither, -1 with an error pending. */ diff --git a/packages/runtime/src/scr_dyn_for_await.c b/packages/runtime/src/scr_dyn_for_await.c index 25183c4ee..3a1a6458d 100644 --- a/packages/runtime/src/scr_dyn_for_await.c +++ b/packages/runtime/src/scr_dyn_for_await.c @@ -38,6 +38,11 @@ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { return iterator; } +/* For an iterator another open path made (scr_dyn_for_await_stream.c). */ +void scr_dyn_async_iterator_set_sync(bool sync) { + scr_for_await_sync = sync; +} + /* Whether the iterator the last scr_dyn_async_iterator_open returned is a * synchronous one. Read immediately after the open. */ bool scr_dyn_async_iterator_was_sync(void) { diff --git a/packages/runtime/src/scr_dyn_for_await_stream.c b/packages/runtime/src/scr_dyn_for_await_stream.c new file mode 100644 index 000000000..6facf916c --- /dev/null +++ b/packages/runtime/src/scr_dyn_for_await_stream.c @@ -0,0 +1,61 @@ +/* `for await` over a checked-dynamic value in a program that links the + * stream unit: a native readable stream behind the value (a boxed stream, + * or a Cipheriv/Decipheriv handle's Transform) iterates through an + * iterator object whose next() answers the readable iterator's step as + * { value, done } and whose return() closes it as a typed loop does + * (destroy with an AbortError). Any other value takes + * scr_dyn_async_iterator_open. Linked only by programs with such a loop + * and streams. */ +#include "scr_runtime.h" +#include "scr_stream.h" + +/* caps[0]: the stream */ +static ScrStream *iter_stream(ScrClosure *clo) { + return scr_box_get_ref(clo->caps[0]); /* +1 */ +} + +static ScrDyn *iter_next_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = iter_stream(clo); + ScrPromise *p = scr_stream_next_result_dyn(s); + scr_stream_release(s); + ScrDyn *d = scr_dyn_new_promise(p); + scr_promise_release(p); + return d; +} + +static ScrDyn *iter_return_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrStream *s = iter_stream(clo); + scr_stream_release(scr_stream_iterator_close(s)); + scr_stream_release(s); + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, scr_dyn_retain(scr_dyn_undefined())); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(true)); + ScrPromise *p = scr_promise_new(); + scr_promise_fulfill_ref(p, result, scr_dyn_retain_v, scr_dyn_release_v, NULL); + ScrDyn *d = scr_dyn_new_promise(p); + scr_promise_release(p); + return d; +} + +static ScrDyn *iter_method(ScrStream *s, ScrDynThunk thunk, const char *name) { + ScrClosure *clo = scr_closure_new((void *)thunk, 1); + clo->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(clo->caps[0], scr_stream_retain(s)); + return scr_dyn_new_func(clo, thunk, 0, "()", name); +} + +ScrDyn *scr_stream_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { + ScrStream *s = scr_stream_native_readable_of_dyn(value); + if (!s) return scr_dyn_async_iterator_open(value, spell); + /* the loop awaits its results with the async protocol */ + scr_dyn_async_iterator_set_sync(false); + ScrDyn *it = scr_dyn_new_obj(); + scr_dyn_obj_set(it, "next", 4, iter_method(s, &iter_next_thunk, "next")); + scr_dyn_obj_set(it, "return", 6, iter_method(s, &iter_return_thunk, "return")); + scr_stream_release(s); + return it; +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 75debe615..4a53da4c0 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -145,9 +145,12 @@ struct ScrStreamState { bool object_mode_opt; /* The parked for-await next() promise (at most one — the loop awaits * each chunk before asking again). next_dyn picks the resolution - * shape: a dyn boxed by tag (the JS lane), or bytes. */ + * shape: a dyn boxed by tag (the JS lane), or bytes; next_result + * wraps the dyn in an iterator result ({ value, done }) for the + * checked-dynamic for-await protocol. */ ScrPromise *next_waiter; bool next_dyn; + bool next_result; /* The iterator's own read() is calling _read: synchronous pushes * buffer, and the waiter takes the whole buffer afterwards (Node's * read() returns everything _read pushed). */ @@ -1226,7 +1229,15 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, scr_promise_release(w); return; } - if (dyn) { + if (dyn && st->r.next_result) { + /* { value, done }: the value, or undefined at the end */ + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, + chunk == NULL ? scr_dyn_retain(scr_dyn_undefined()) + : scr_stream_entry_box(chunk, kind)); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(chunk == NULL)); + payload = result; + } else if (dyn) { payload = chunk == NULL ? scr_stream_from_done() : scr_stream_entry_box(chunk, kind); } else { payload = chunk ? (ScrBytes *)chunk : scr_bytes_new(SCR_BYTES_U8, 0); @@ -1352,11 +1363,12 @@ static void scr_stream_settle_next(ScrStream *s) { } } -static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { +static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn, bool result) { ScrStreamState *st = s->st; ScrPromise *p = scr_promise_new(); if (!st->has_r) { st->r.next_dyn = dyn; + st->r.next_result = result; scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, SCR_ENTRY_BYTES); return p; } @@ -1368,6 +1380,7 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { return p; } st->r.next_dyn = dyn; + st->r.next_result = result; st->r.next_waiter = scr_promise_retain(p); st->r.next_parked = false; if (!st->r.next_started) { @@ -1391,11 +1404,15 @@ ScrPromise *scr_stream_next_chunk(ScrStream *s) { scr_promise_reject_pending(p); return p; } - return scr_stream_next_chunk_impl(s, false); + return scr_stream_next_chunk_impl(s, false, false); } ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s) { - return scr_stream_next_chunk_impl(s, true); + return scr_stream_next_chunk_impl(s, true, false); +} + +ScrPromise *scr_stream_next_result_dyn(ScrStream *s) { + return scr_stream_next_chunk_impl(s, true, true); } /* ── Readable.from (array, string and Buffer sources) ─────────────────── */ @@ -7524,6 +7541,18 @@ static ScrStream *scr_stream_boxed(const ScrDyn *value) { return NULL; } +/* The native readable stream behind a checked-dynamic value (+1): a + * boxed stream or a Cipheriv/Decipheriv handle's Transform, or NULL. */ +ScrStream *scr_stream_native_readable_of_dyn(const ScrDyn *value) { + ScrStream *s = scr_stream_of_handle(value); + if (!s) s = scr_stream_boxed(value); + if (s && !s->st->has_r) { + scr_stream_release(s); + return NULL; + } + return s; +} + /* stream.handleStream: the stream of a value typed as a stream handle * class (a Cipheriv or Decipheriv), +1. A value of that type can also be * the stream itself, boxed (the stream addAbortSignal answers). */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 0e0416e05..623f60465 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -301,6 +301,14 @@ bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb); ScrPromise *scr_stream_next_chunk(ScrStream *s); ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s); +/* The same step resolving with an iterator result ({ value, done }). */ +ScrPromise *scr_stream_next_result_dyn(ScrStream *s); +/* The native readable stream behind a checked-dynamic value (+1), or NULL. */ +ScrStream *scr_stream_native_readable_of_dyn(const struct ScrDyn *value); +/* for await over a checked-dynamic value in a program with streams + * (scr_stream_ops.c): a native readable stream iterates as a typed loop + * does; any other value takes scr_dyn_async_iterator_open. */ +struct ScrDyn *scr_stream_dyn_async_iterator_open(struct ScrDyn *value, ScrStr *spell); /* node:stream/promises — the promise forms over the finished/pipeline * machinery above: a pending void promise the terminal watcher settles * (fulfilled on a clean finish, rejected with the finish status diff --git a/tests/corpus/7420-for-await-close.ts b/tests/corpus/7420-for-await-close.ts new file mode 100644 index 000000000..52930a02c --- /dev/null +++ b/tests/corpus/7420-for-await-close.ts @@ -0,0 +1,100 @@ +// Leaving a for await loop over an async generator early closes it, as +// AsyncIteratorClose does: return, throw and break inside the body, a +// labeled continue to an outer loop, and a return() at a yield in the +// body all await the generator's return(), so its finally runs before the +// loop's caller continues. An exhausted generator and a rejected next() +// are not closed. After a throw, an error from closing is dropped; after +// a break or return it propagates. +const log: string[] = []; + +async function* numbers(tag: string, failClose = false): AsyncGenerator { + try { + yield 1; + yield 2; + yield 3; + } finally { + log.push(`${tag} finally`); + if (failClose) throw new Error(`${tag} close failed`); + } +} + +async function* slowClose(tag: string): AsyncGenerator { + try { + yield 1; + } finally { + await null; + log.push(`${tag} finally after await`); + } +} + +async function firstOf(tag: string): Promise { + for await (const n of numbers(tag)) return n * 10; + return -1; +} + +async function throwsInside(tag: string): Promise { + for await (const n of numbers(tag)) throw new Error(`${tag} body ${n}`); +} + +async function* takeOne(tag: string): AsyncGenerator { + for await (const n of numbers(tag)) { + yield n; + return; + } +} + +async function* rethrowing(tag: string): AsyncGenerator { + for await (const n of numbers(tag)) { + if (n === 2) throw new Error(`${tag} at ${n}`); + yield `${tag} ${n}`; + } +} + +async function main(): Promise { + log.push(`first ${await firstOf("return")}`); + try { + await throwsInside("throw"); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + for await (const n of takeOne("generator-return")) log.push(`took ${n}`); + try { + for await (const s of rethrowing("generator-throw")) log.push(s); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + const early = takeOne("consumer-return"); + for await (const n of early) { + log.push(`consumer got ${n}`); + break; + } + try { + for await (const n of numbers("break-close-fails", true)) { + log.push(`break at ${n}`); + break; + } + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + try { + for await (const n of numbers("throw-close-fails", true)) throw new Error(`body ${n}`); + } catch (e) { + log.push(`caught ${(e as Error).message}`); + } + void Promise.resolve().then(() => log.push("microtask")); + for await (const n of slowClose("slow")) { + log.push(`slow ${n}`); + break; + } + log.push("after slow"); + outer: for (const k of [1, 2]) { + for await (const n of numbers(`labeled ${k}`)) { + log.push(`labeled ${k} got ${n}`); + continue outer; + } + } + for await (const n of numbers("exhausted")) log.push(`exhausted ${n}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7421-for-await-close.mjs b/tests/corpus/7421-for-await-close.mjs new file mode 100644 index 000000000..c891a2084 --- /dev/null +++ b/tests/corpus/7421-for-await-close.mjs @@ -0,0 +1,53 @@ +// The same early exits in JavaScript: a for await loop that returns, +// throws or breaks closes its async generator, including one that reaches +// the loop as an untyped parameter, and a source whose return() is still +// running finishes before the code after the loop. +const log = []; + +async function* letters(tag) { + try { + yield "a"; + yield "b"; + } finally { + log.push(`${tag} finally`); + } +} + +async function firstLetter(source) { + for await (const c of source) return c; +} + +async function failOn(source, target) { + for await (const c of source) { + if (c === target) throw new Error(`failed on ${c}`); + log.push(`kept ${c}`); + } +} + +async function* upperUntil(source, stop) { + for await (const c of source) { + if (c === stop) return; + yield c.toUpperCase(); + } +} + +log.push(`first ${await firstLetter(letters("param-return"))}`); +try { + await failOn(letters("param-throw"), "b"); +} catch (e) { + log.push(`caught ${e.message}`); +} +for await (const c of upperUntil(letters("stage-return"), "b")) log.push(`upper ${c}`); +let ticks = 0; +const tick = () => { + log.push(`tick ${ticks}`); + if (++ticks < 4) process.nextTick(tick); +}; +process.nextTick(tick); +for await (const c of letters("break")) { + log.push(`break ${c}`); + break; +} +log.push("after break"); +await new Promise((resolve) => setTimeout(resolve, 1)); +console.log(log.join("\n")); diff --git a/tests/corpus/7422-for-await-untyped-streams.mjs b/tests/corpus/7422-for-await-untyped-streams.mjs new file mode 100644 index 000000000..b7dd21072 --- /dev/null +++ b/tests/corpus/7422-for-await-untyped-streams.mjs @@ -0,0 +1,38 @@ +// for await over a native stream that reaches the loop as an untyped +// value iterates it as a typed loop does, in the same tick order as Node: +// whole reads, an early return (the stream is destroyed), a break, an +// object-mode Transform and a stream error. +import { Readable, PassThrough, Transform } from "node:stream"; +const log = []; +async function readAll(s, tag) { + for await (const c of s) log.push(`${tag} ${typeof c} ${String(c)}`); + log.push(`${tag} done`); +} +async function first(s, tag) { + for await (const c of s) return `${tag} first ${String(c)}`; +} +async function breaks(s, tag) { + for await (const c of s) { log.push(`${tag} got ${String(c)}`); break; } +} +async function failing(s, tag) { + try { for await (const c of s) log.push(`${tag} ${c}`); } catch (e) { log.push(`${tag} caught ${e.message}`); } +} +let n = 0; +const ticker = () => { log.push(`tick ${n}`); if (++n < 6) process.nextTick(ticker); }; +process.nextTick(ticker); +await readAll(Readable.from(["a", "b"]), "arr"); +const r = Readable.from(["x", "y", "z"]); +r.on("close", () => log.push(`r close ${r.destroyed}`)); +log.push(await first(r, "r")); +const pt = new PassThrough(); +pt.on("close", () => log.push("pt close")); +pt.end("bytes"); +await breaks(pt, "pt"); +const t = new Transform({ objectMode: true, transform(c, e, cb) { cb(null, { v: c }); } }); +t.end(1); +await (async (s) => { for await (const c of s) log.push(`t ${JSON.stringify(c)}`); })(t); +const bad = new Readable({ read() { this.destroy(new Error("boom")); } }); +await failing(bad, "bad"); +await readAll(["plain", "array"], "dyn"); +await new Promise((res) => setTimeout(res, 5)); +console.log(log.join("\n")); diff --git a/tests/corpus/7423-stream-untyped-bindings.mjs b/tests/corpus/7423-stream-untyped-bindings.mjs new file mode 100644 index 000000000..23b9e1411 --- /dev/null +++ b/tests/corpus/7423-stream-untyped-bindings.mjs @@ -0,0 +1,33 @@ +// Streams held where the lowering keeps a checked-dynamic value: a +// module-level binding of a stream method's result on a value that a +// JavaScript function returns, an element read of an array of streams, +// and readable.compose on a stream whose function is typed with JSDoc. +import { PassThrough, Readable, Transform } from "node:stream"; + +const words = () => Readable.from(["alpha", "beta"]); +const shouted = words().map((w) => w.toUpperCase()); +console.log("shouted", shouted.readable, shouted.readableObjectMode); +for await (const w of shouted) console.log("shouted", w); + +const counted = words().filter((w) => w.length > 4); +counted.on("data", (w) => console.log("counted", w)); +await new Promise((resolve) => counted.on("end", resolve)); +console.log("counted ended", counted.readableEnded); + +const streams = [new PassThrough({ objectMode: true }), new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + cb(null, { wrapped: chunk }); + }, +})]; +streams[1].end(7); +for await (const v of streams[1]) console.log("element", JSON.stringify(v)); +streams[0].end("done"); +for await (const v of streams[0]) console.log("element", v); + +/** @returns {Readable} */ +const typedWords = () => Readable.from(["x", "yy"]); +const lengths = typedWords().compose(async function* (source) { + for await (const w of source) yield `${w}:${w.length}`; +}); +console.log("composed", (await lengths.toArray()).join(",")); diff --git a/tests/harness/corpus-costs/7420-for-await-close.ts.json b/tests/harness/corpus-costs/7420-for-await-close.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7420-for-await-close.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } diff --git a/tests/harness/corpus-costs/7421-for-await-close.mjs.json b/tests/harness/corpus-costs/7421-for-await-close.mjs.json new file mode 100644 index 000000000..cb701dc93 --- /dev/null +++ b/tests/harness/corpus-costs/7421-for-await-close.mjs.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 2600 } diff --git a/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json b/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json new file mode 100644 index 000000000..cb701dc93 --- /dev/null +++ b/tests/harness/corpus-costs/7422-for-await-untyped-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 2600 } diff --git a/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json b/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json new file mode 100644 index 000000000..d2ac5db99 --- /dev/null +++ b/tests/harness/corpus-costs/7423-stream-untyped-bindings.mjs.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 6454 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c4013d288..f143419dc 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From ef8ee2b3939b342b83be6a169ac256b7ba77cf84 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:03:33 -0500 Subject: [PATCH 039/169] Emit a debug build's function bodies in one emitter Debug metadata is numbered per emitter (debug-info.ts) and the range journal does not carry it, so a debug build emitted in ranges or on workers referred to metadata the merged module did not define. Debug builds now emit every function body in the emitter itself. --- .../compiler/src/backend/llvm/emit-module.ts | 7 ++- packages/compiler/src/backend/llvm/emitter.ts | 10 ++-- .../compiler/test/chunked-emission.test.ts | 53 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 75e6ad50f..1f66e566f 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -192,10 +192,13 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st export function emitModuleParts(emitter: LlEmitter): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. + // Debug metadata is numbered per emitter and not journaled, so a debug + // build emits every body here. + const ranges = emitter.debug === null; const chunks = emitter.targetOptions.functionChunks ?? 1; const fnDefs = - (chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? - emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ?? + (ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? + (ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ?? []; if (fnDefs.length === 0) for (const fn of emitter.mod.functions) { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1ca9313b7..2daabdb92 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -414,11 +414,13 @@ export interface LlvmTargetOptions { targetTriple?: string; /** Emit the function bodies in this many contiguous ranges, each by its * own emitter, and merge them in function order (emit-journal.ts). The - * output is the same as one emitter's. */ + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ functionChunks?: number; /** Emit the function bodies of a large module on this many forked worker * processes at a time (emit-module.ts), forked with `forkPrimitives`. The - * output is the same as one emitter's. */ + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ functionWorkers?: number; /** The host's process primitives, where it can fork (the native * compiler's). */ @@ -1273,7 +1275,9 @@ export class LlEmitter { this.journal = journal; this.journaled = journal !== null || - ((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined); + ((options.functionWorkers ?? 1) > 1 && + options.forkPrimitives !== undefined && + options.debugSources === undefined); this.stackTraces = observesStacks(mod); this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined); this.mockTimers = this.runtimeFeatures.mockTimers; diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index 9902569f7..b4912c9f6 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -67,3 +67,56 @@ test("function bodies emitted in chunks merge into the text one emitter gives", await rm(dir, { recursive: true, force: true }); } }, 600_000); + +// Debug metadata is numbered per emitter (debug-info.ts) and not journaled: +// a debug build emits its function bodies in one emitter. +test("debug builds emit function bodies serially, with every metadata reference defined", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-")); + const minFunctions = process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + try { + const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging"); + const entry = join(corpus, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "ir", + optimization: "dev", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ir = await readFile(outPath, "utf8"); + const debugSources = new Map(); + for (const name of ["main.ts", "helper.ts"]) + debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); + const options: LlvmTargetOptions = { debugSources }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial).toContain("!DILocation"); + + let forks = 0; + process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; + const forkPrimitives = { + fork: (): number => { + forks++; + return -1; + }, + exit: (): void => {}, + wait: (): number => -1, + }; + for (const variant of [ + { functionChunks: 3 }, + { functionWorkers: 4, forkPrimitives }, + ] satisfies LlvmTargetOptions[]) { + const emitted = text(emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant })); + expect(emitted === serial, JSON.stringify(Object.keys(variant))).toBe(true); + const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1])); + const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1])); + expect([...referenced].filter((id) => !defined.has(id))).toEqual([]); + } + expect(forks).toBe(0); + } finally { + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; + await rm(dir, { recursive: true, force: true }); + } +}, 120_000); From dc9b60656d010567bb47c6c4c4f4ba14a3254f4e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:11:34 -0500 Subject: [PATCH 040/169] Index arrays with exact i64 products of int32 values `a[i * n + k]` with int32 operands kept the index in doubles whenever the product's range could pass 2^53: integer arithmetic needs a proven safe range, and an int32 product can reach 2^62. Every access then converted the double back with an exactness test. Such arithmetic now also carries a wide view: the exact integer as an i64, the magnitude it is bounded by, and a threshold below which the double equals it. A product of two exact integers equals its double below 2^53 in magnitude, and a sum or difference of a wide view exact below T and an exact integer of magnitude at most B equals its double below min(T - B, 2^53). Every i64 result stays within 2^62 + 2^53, so none wraps. An array index below a length or capacity is below 2^32, under every threshold kept, so the dense array guards and local array reads compare the wide view, unsigned, with the array's counts and skip the round trip. Any index that fails those comparisons, including every index whose double rounds, takes the double's path as before. Typed arrays and strings are unchanged. --- .../src/backend/llvm/dense-array-access.ts | 15 ++-- .../compiler/src/backend/llvm/expr-context.ts | 12 +++ .../src/backend/llvm/expr-primitives.ts | 10 ++- .../src/backend/llvm/integer-values.ts | 85 ++++++++++++++++++- .../src/backend/llvm/local-array-reads.ts | 22 +++-- .../test/array-index-product-emission.test.ts | 55 ++++++++++++ ...325-array-index-products-near-2-53.ts.json | 6 ++ .../7325-array-index-products-near-2-53.ts | 82 ++++++++++++++++++ ...325-array-index-products-near-2-53.ts.json | 1 + 9 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/array-index-product-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json create mode 100644 tests/corpus/7325-array-index-products-near-2-53.ts create mode 100644 tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 4488f90cb..ba3b48b02 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -27,7 +27,7 @@ import { storeArrCount, } from "./common.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; -import { exactInteger, widenInteger } from "./integer-values.js"; +import { indexInteger } from "./integer-values.js"; const SCR_ARR_VALUE = 1; @@ -74,13 +74,16 @@ function emitDenseGuard( ): DenseGuard { const B = host.B; const size = host.sizeType; - const integer = exactInteger(host, index, indexExpr); + const integer = indexInteger(host, index, indexExpr); let wide: string; let integral: string | null = null; - if (integer) { - // A proven integer needs no double round-trip. Negative signed values - // compare above any length or capacity as unsigned 64-bit integers. - wide = widenInteger(host, integer); + if (integer !== null) { + // A proven integer, or a wide view that equals the double at every + // index below a length, needs no double round-trip. Negative signed + // values compare above any length or capacity as unsigned 64-bit + // integers, and every index that fails the comparisons takes the + // double's path. + wide = integer; } else { // freeze keeps NaN/out-of-range conversions from producing poison; the // exact round-trip comparison then rejects every value that is not an diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index 9dd6bbcac..df6dfc608 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -15,11 +15,23 @@ export interface LlValue { /** An exact integer snapshot, excluding negative zero, with no discarded * high bits and the proven range of its original value. */ integer?: { name: string; type: "i32" | "i64"; signed: boolean; range: IntegerRange }; + /** An i64 holding the exact integer that the arithmetic producing this + * number computes, when that integer has no proven safe range: its + * absolute value is at most `bound`, and the double equals it whenever + * its absolute value is below `below` (a zero may be -0). Index consumers + * use it in place of a round trip (integer-values.ts `indexInteger`). */ + wideInteger?: WideInteger; slot?: boolean; /** A sequence local held in a capture box; release the box itself. */ boxed?: boolean; } +export interface WideInteger { + name: string; + below: number; + bound: number; +} + export type ExprOf = Extract; export type LibCallExpr = ExprOf<"libCall">; type LibCallPrefixOf = T extends `${infer Prefix}.${string}` ? Prefix : never; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 354004e9e..4d3904bde 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -16,6 +16,7 @@ import { int32Number, integerNumber, narrowInteger, + wideArithmetic, widenInteger, } from "./integer-values.js"; import { @@ -337,7 +338,14 @@ export function emitOperatorExpr( if (arith[e.op] !== undefined) { B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`); const uint32 = emitWrappingSum(host, e, l, r, li, ri); - if (uint32 !== undefined) return { name: t, type: e.type, uint32 }; + // The exact integer beside the double, for index consumers. + const wideInteger = wideArithmetic(host, e.op, l, li, r, ri); + if (uint32 !== undefined || wideInteger) { + const value: LlValue = { name: t, type: e.type }; + if (uint32 !== undefined) value.uint32 = uint32; + if (wideInteger) value.wideInteger = wideInteger; + return value; + } } else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`); } else if (bit[e.op] !== undefined) { if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") { diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts index 63e279d04..ddbcdd8da 100644 --- a/packages/compiler/src/backend/llvm/integer-values.ts +++ b/packages/compiler/src/backend/llvm/integer-values.ts @@ -1,6 +1,6 @@ import type { IrExpr, IrType } from "../../ir/ir.js"; import type { IntegerRange } from "../../ir/integer-ranges.js"; -import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import type { LlValue, LlvmEmitterContext, WideInteger } from "./expr-context.js"; export type LlInteger = NonNullable; @@ -51,6 +51,89 @@ export function exactInteger( return { name, type: "i64", signed: true, range }; } +/** Doubles hold every integer of smaller magnitude exactly. */ +const EXACT_DOUBLES = 2 ** 53; +/** Wide views stay within this magnitude: a product of two int32s reaches + * 2^62, one exact term of at most 2^53 may be added to it, and no i64 + * result wraps. */ +const WIDE_BOUND = 2 ** 62 + 2 ** 53; +/** Array and string indexes are below 2^32 - 1: a wide view exact below + * this many is exact at every index a length can admit. */ +const INDEX_LIMIT = 2 ** 32; + +function magnitude(range: IntegerRange): number { + return Math.max(Math.abs(range.min), Math.abs(range.max)); +} + +/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands + * have no proven result range (their product or sum may pass 2^53). + * + * - A product of two exact integers equals its double whenever its + * magnitude is below 2^53: the double product of integers is exact there. + * - A sum or difference of two exact integers likewise. + * - A sum or difference of a wide view (exact below T) and an exact integer + * of magnitude at most B equals its double below min(T - B, 2^53): the + * view's magnitude is then below T, so its double is exact, and the + * double sum of two exact integers below 2^53 is exact. + * + * Every i64 result stays within 2^62 + 2^53 in magnitude, so none wraps. A view + * exact below fewer than 2^32 helps no index and is not kept. */ +export function wideArithmetic( + host: LlvmEmitterContext, + op: string, + l: LlValue, + li: LlInteger | null, + r: LlValue, + ri: LlInteger | null, +): WideInteger | null { + if (op !== "+" && op !== "-" && op !== "*") return null; + const view = (value: LlValue, integer: LlInteger | null): WideInteger | null => + integer + ? { name: "", below: Infinity, bound: magnitude(integer.range) } + : (value.wideInteger ?? null); + const lv = view(l, li), + rv = view(r, ri); + if (!lv || !rv) return null; + let below: number, bound: number; + if (op === "*") { + if (lv.below !== Infinity || rv.below !== Infinity) return null; + below = EXACT_DOUBLES; + bound = lv.bound * rv.bound; + } else if (lv.below === Infinity && rv.below === Infinity) { + below = EXACT_DOUBLES; + bound = lv.bound + rv.bound; + } else if (lv.below === Infinity || rv.below === Infinity) { + const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv]; + below = Math.min(wide.below - exact.bound, EXACT_DOUBLES); + bound = exact.bound + wide.bound; + } else return null; + if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null; + const operand = (value: LlValue, integer: LlInteger | null): string => + integer ? widenInteger(host, integer) : value.wideInteger!.name; + const left = operand(l, li), + right = operand(r, ri); + const name = host.B.tmp(); + host.B.line( + `${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`, + ); + return { name, below, bound }; +} + +/** An i64 index for a consumer that compares it, unsigned, with a length or + * capacity below 2^32 and takes the double's path otherwise: the exact + * integer of `value`, or its wide view, which equals the double at every + * such index. Null when the index needs the double round trip. */ +export function indexInteger( + host: LlvmEmitterContext, + value: LlValue, + expr?: IrExpr, +): string | null { + const integer = exactInteger(host, value, expr); + if (integer) return widenInteger(host, integer); + const wide = value.wideInteger; + return wide && wide.below >= INDEX_LIMIT ? wide.name : null; +} + export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string { if (value.type === "i64") return value.name; const wide = host.B.tmp(); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index cdd00c4e7..b9fc42392 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -13,7 +13,7 @@ import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { ReferenceEffects } from "./reference-effects.js"; import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js"; -import { exactInteger, widenInteger } from "./integer-values.js"; +import { indexInteger } from "./integer-values.js"; import { READ_FALLBACK } from "./dense-array-access.js"; export interface LocalArrayRead { @@ -605,11 +605,11 @@ export function emitBorrowedArrayRead( return; } const index = host.emitExpr(read.index); - // A proven integer index (an int32 local, say) needs no double - // round-trip; a negative one compares above the capacity as unsigned. - const exact = - loopIndex === null && host.sizeType === "i64" ? exactInteger(host, index, read.index) : null; - const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null); + // A proven integer index (an int32 local, say), or a wide view exact at + // every index below the capacity, needs no double round-trip; a negative + // one compares above the capacity as unsigned. + const integerIndex = + loopIndex ?? (host.sizeType === "i64" ? indexInteger(host, index, read.index) : null); const slow = B.newLabel("borrow.array.slow"), join = B.newLabel("borrow.array.join"); emitDenseReferenceArrayRead( @@ -644,8 +644,12 @@ export function emitLocalArrayRead( ): { slot: string; type: IrType } | null { const B = host.B; const array = host.emitStableReceiver(read.array, [read.index]); - const integerIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; + const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null; const index = host.emitExpr(read.index); + // As in emitBorrowedArrayRead: an exact integer or wide view needs no + // round trip, and only the loop's own counter is an induction variable. + const integerIndex = + loopIndex ?? (inline && host.sizeType === "i64" ? indexInteger(host, index, read.index) : null); const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null; if (nullable) { // A nullable union is the element pointer itself: borrowed from an @@ -665,7 +669,7 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, store, no, slow, @@ -726,7 +730,7 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, storeValue, no, slow, diff --git a/packages/compiler/test/array-index-product-emission.test.ts b/packages/compiler/test/array-index-product-emission.test.ts new file mode 100644 index 000000000..bfd891398 --- /dev/null +++ b/packages/compiler/test/array-index-product-emission.test.ts @@ -0,0 +1,55 @@ +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 } from "../src/index.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-index-product-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function at(a: number[], x: number, y: number, k: number): number | undefined { + return a[x * y + k]; +} +function cancel(a: number[], p: number, q: number, r: number, s: number): number | undefined { + return a[p * q - r * s]; +} +const a = [1, 2, 3]; +const v = process.argv.length; +console.log(at(a, v | 0, (v + 1) | 0, -v | 0), cancel(a, v | 0, v | 0, v | 0, v | 0)); +`; + +function body(llvm: string, name: string): string { + return new RegExp(`^define [^@]*@sc_\\w+?_${name}(\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; +} + +test("an int32 product plus an int32 indexes as an exact i64 without a round trip", async () => { + const llvm = await emitLlvm(source); + // The int32 entry: the index is the i64 product and sum, compared with + // the array's counts; the double goes only to the slow path's runtime call. + const at = /^define [^@]*@sc_bf_at\.i32\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const product = /(%\S+) = mul nsw i64 %\S+, %\S+/.exec(at)?.[1]; + expect(product).toBeDefined(); + expect(at).toContain(`= add nsw i64 ${product}, `); + expect(at).not.toMatch(/fptosi double %\S+ to i64/); +}); + +test("a difference of two int32 products keeps the double's round trip", async () => { + const llvm = await emitLlvm(source); + // Each product may pass 2^53, where its double rounds before the + // difference: the index keeps the exactness test of the double. + const cancel = body(llvm, "cancel"); + expect(cancel).toMatch(/fptosi double %\S+ to i64/); + expect(cancel).toMatch(/fcmp oeq double/); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json new file mode 100644 index 000000000..3239c26bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7325-array-index-products-near-2-53.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7325-array-index-products-near-2-53.ts b/tests/corpus/7325-array-index-products-near-2-53.ts new file mode 100644 index 000000000..41e60b09d --- /dev/null +++ b/tests/corpus/7325-array-index-products-near-2-53.ts @@ -0,0 +1,82 @@ +// Element reads and writes indexed by products of int32 values plus or +// minus int32 terms. The products can pass the int32 range and 2^53, where +// a double product rounds: an index that lands in an array's range reads +// and writes that element, and every other one (negative, past the length, +// rounded past 2^53) keeps the property key Node computes from the double. + +function show(label: string, value: unknown): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function read(a: number[], x: number, y: number, k: number): number | undefined { + return a[x * y + k]; +} + +function readBack(a: number[], x: number, y: number, k: number): number | undefined { + return a[x * y - k]; +} + +function write(a: number[], x: number, y: number, k: number, v: number): void { + a[x * y + k] = v; +} + +function rows(n: number, cols: number, a: number[]): number { + let s = 0; + for (let i = 0; i < n; i++) for (let j = 0; j < cols; j++) s += a[i * cols + j]! * (j + 1); + return s; +} + +const a: number[] = []; +for (let i = 0; i < 64; i++) a.push(i * 10); + +const big = 2147483647; +const half = 65536; + +// In range. +show("3*5+2", read(a, 3 | 0, 5 | 0, 2 | 0)); +show("7*-1+9", read(a, 7 | 0, -1 | 0, 9 | 0)); +show("-0 index", read(a, -1 | 0, 0 | 0, 0 | 0)); +// The product passes the int32 range; the difference lands in range. +show("2^16*2^15-2^31+12", readBack(a, half | 0, 32768 | 0, (-2147483648 * -1 - 12) | 0)); +show("(2^31-1)*1-(2^31-1)+5", read(a, big | 0, 1 | 0, -2147483642 | 0)); +show("2^16*2^16-big+20", readBack(a, half | 0, half | 0, (big - 20) | 0)); +// Out of range or negative: holes and named keys. +show("past end", read(a, 8 | 0, 8 | 0, 1 | 0)); +show("negative", read(a, -3 | 0, 5 | 0, 0 | 0)); +show("2^31*2^31", read(a, -2147483648 | 0, -2147483648 | 0, 7 | 0)); + +// Products around 2^53, where the double product rounds. +const keys: number[] = []; +const x = 134217727; // 2^27 - 1 +const y = 67108865; // 2^26 + 1: x * y = 2^53 + 2^26 - 1 +const near: [number, number, number][] = [ + [x, y, 0], + [x, y, 1], + [x, y, -67108864], + [x, y, -67108863], + [94906265, 94906265, 0], + [94906267, 94906267, -3], + [-2147483648, 2147483647, 2147483647], +]; +for (const [p, q, k] of near) { + const sparse: number[] = []; + write(sparse, p | 0, q | 0, k | 0, 1); + // The key the double computes; a local holds only the double. + const key: number = p * q + k; + keys.push(key); + show(`write ${p}*${q}+${k}`, `${sparse[key]} ${sparse[key + 1]} ${sparse[key - 1]} ${sparse.length}`); + show(`read ${p}*${q}+${k}`, read(sparse, p | 0, q | 0, k | 0)); +} +show("keys", keys.join(",")); + +// A difference of two products above 2^53 keeps the double's rounding. +function cancel(sparse: number[], p: number, q: number, r: number, s: number): number | undefined { + return sparse[p * q - r * s]; +} +const cancelled: number[] = [7, 8, 9]; +show("cancel", cancel(cancelled, x | 0, y | 0, x | 0, y | 0)); +show("cancel+1", cancel(cancelled, x | 0, y | 0, (9007199254740992 / 67108864) | 0, 67108864 | 0)); + +// Row-major sums with a width from JSON. +const dims = JSON.parse("[[8, 8], [4, 16], [16, 4], [3, 7]]") as [number, number][]; +for (const [n, cols] of dims) show(`rows ${n}x${cols}`, rows(n | 0, cols | 0, a)); diff --git a/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json b/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json new file mode 100644 index 000000000..f71f3eb98 --- /dev/null +++ b/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2400 } From d8d75dfa189e3e7d4fce34b285d1dc100dc85dbe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:32:25 -0500 Subject: [PATCH 041/169] Support Duplex.from over streams, pairs, promises and async functions --- .../builtin-apis/streams-and-readline.mdx | 5 +- packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/lib-shared.ts | 10 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 149 +++++++- packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 10 + packages/compiler/src/ir/runtime-features.ts | 7 + packages/compiler/src/ir/validate.ts | 30 +- .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7424-duplex-from-streams.mjs.json | 6 + .../corpus/7425-duplex-from-streams.ts.json | 6 + .../corpus/7426-duplex-from-promise.mjs.json | 6 + .../7427-duplex-from-async-function.mjs.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_stream.c | 360 +++++++++++++++++- packages/runtime/src/scr_stream.h | 14 + packages/runtime/src/scr_stream_ops.c | 168 +++++++- tests/corpus/7424-duplex-from-streams.mjs | 39 ++ tests/corpus/7425-duplex-from-streams.ts | 72 ++++ tests/corpus/7426-duplex-from-promise.mjs | 27 ++ .../7427-duplex-from-async-function.mjs | 40 ++ .../7424-duplex-from-streams.mjs.json | 1 + .../7425-duplex-from-streams.ts.json | 1 + .../7426-duplex-from-promise.mjs.json | 1 + .../7427-duplex-from-async-function.mjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 32 files changed, 958 insertions(+), 29 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json create mode 100644 tests/corpus/7424-duplex-from-streams.mjs create mode 100644 tests/corpus/7425-duplex-from-streams.ts create mode 100644 tests/corpus/7426-duplex-from-promise.mjs create mode 100644 tests/corpus/7427-duplex-from-async-function.mjs create mode 100644 tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json create mode 100644 tests/harness/corpus-costs/7425-duplex-from-streams.ts.json create mode 100644 tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json create mode 100644 tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 060a0bf95..cfd52e054 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -22,8 +22,9 @@ `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. `Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable. - A lone `Readable` or `Writable`, a promise, a `{ readable, writable }` pair, a web stream, and a body function that declares the options argument are unsupported in `Duplex.from`. - `stream.compose` accepts streams and async generator functions in any position, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. + It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`. + A web stream and a body function that declares the options argument are unsupported in `Duplex.from`. + `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. Web streams as compose stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 484b03e8b..1f2fb241a 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, + runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v29"); + expect(error?.stderr).toContain("scr_runtime_abi_v30"); }); }); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 94318c3aa..a790ad197 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1129,6 +1129,11 @@ export const LIB_FN_SYMS: Record = { "stream.decodeStrings": "scr_stream_decode_strings", "stream.duplexFromGen": "scr_stream_duplex_from_gen", "stream.duplexFromDyn": "scr_stream_duplex_from_dyn", + "stream.duplexifyReadable": "scr_stream_duplexify_readable", + "stream.duplexifyWritable": "scr_stream_duplexify_writable", + "stream.duplexifyPair": "scr_stream_duplexify_pair", + "stream.duplexFromPromise": "scr_stream_duplex_from_promise", + "stream.duplexFromAsyncFn": "scr_stream_duplex_from_async_fn", "writable.writeEncoding": "scr_stream_write_encoding", "writable.setDefaultEncoding": "scr_stream_set_default_encoding", "stream.chunkDone": "scr_stream_chunk_done", @@ -1737,6 +1742,11 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setConstruct", "stream.duplexFromGen", "stream.duplexFromDyn", + "stream.duplexifyReadable", + "stream.duplexifyWritable", + "stream.duplexifyPair", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", "stream.setWritev", "stream.setWritevDefault", "process.activeResources", diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 948fa6359..fcc8d160e 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 29 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v29" as const; +export const RUNTIME_ABI_VERSION = 30 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v30" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index a97769328..58e2b4855 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, + runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 85038d8c5..b898a3e62 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, + runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6aa8aef96..7c0c2b46c 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -3772,11 +3772,11 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE if (fn !== null) { const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; - if (!isAsync || !isGen) { + if (!isAsync) { lowerer.noLowering( - "Duplex.from over a function that is not an async generator function", + "Duplex.from over a function that is not async", node, - "write the body as async function* (source) { for await (const chunk of source) ... }", + "write the body as async function* (source) { ... } or async function (source) { ... }", ); } if (fn.parameters.nodes.length > 1) { @@ -3794,6 +3794,34 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE nextT: UNDEFINED_T, }; const lowered = lowerStageFunction(lowerer, fn, sourceT); + if (!isGen) { + // an async function: a writable Duplexify whose final waits for + // the function's promise + const asyncSource = duplexSourceHelper(lowerer, sourceT, loc); + const out = lowered.out; + if (out.kind !== "promise" || !asyncSource) + return lowerer.noLowering("Duplex.from over a function with this return type", node); + if ( + out.inner.kind !== "void" && + !canConvertToDyn( + out.inner, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + lowerer.noLowering( + `Duplex.from over a function resolving to '${lowerer.fmt(out.inner)}'`, + node, + ); + } + return { + kind: "libCall", + fn: "stream.duplexFromAsyncFn", + args: [lowered.closure, asyncSource, pipelineBoxHelper(lowerer, out, loc)], + type: duplex, + loc, + }; + } const helpers = lowered.out.kind === "generator" ? fromGenHelpers(lowerer, lowered.out, loc) : null; const source = duplexSourceHelper(lowerer, sourceT, loc); @@ -3818,6 +3846,45 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE } const bodySides = bodyT?.kind === "object" ? streamSidesOf(lowerer, lowerer.classes.get(bodyT.className)) : null; + let literal: ts.Expression = node; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (bodySides === null && ts.isObjectLiteralExpression(literal)) + return duplexFromPair(lowerer, literal, loc); + if (bodyT?.kind === "promise") { + // a Duplexify that reads the promise's value + const valueT = bodyT.inner; + if ( + valueT.kind !== "void" && + !canConvertToDyn( + valueT, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + lowerer.noLowering(`Duplex.from over a promise of '${lowerer.fmt(valueT)}'`, node); + } + const promise = lowerer.lowerExpr(node); + if (promise.type.kind !== "promise") { + // a checked-dynamic promise (an untyped function's result) + return { + kind: "libCall", + fn: "stream.duplexFromDyn", + args: [lowerer.coerceToExpected(promise, DYN)], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } + return { + kind: "libCall", + fn: "stream.duplexFromPromise", + args: [ + promise, + pipelineBoxHelper(lowerer, promise.type as IrType & { kind: "promise" }, loc), + ], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } if (bodySides === null) return lowerFromSource(lowerer, node, node, undefined, true); let stream: IrExpr; try { @@ -3830,16 +3897,88 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE stream.type.kind === "object" ? streamSidesOf(lowerer, lowerer.classes.get(stream.type.className)) : null; + // _duplexify({ readable }) / _duplexify({ writable }) + if (sides === "r" || sides === "w") { + return { + kind: "libCall", + fn: sides === "r" ? "stream.duplexifyReadable" : "stream.duplexifyWritable", + args: [stream], + type: duplex, + loc, + }; + } if (sides !== "rw") { lowerer.noLowering( - `Duplex.from over a ${sides === "r" ? "Readable" : sides === "w" ? "Writable" : "value of this type"}`, + "Duplex.from over a value of this type", node, - "an async generator function, an array, a string, a generator, an async generator, or a Duplex, Transform or PassThrough is the lowered body", + "an async generator function, an iterable, a stream, or a { readable, writable } pair is the lowered body", ); } return stream; } +/** Duplex.from({ readable, writable }): each member that is a stream with + * that side is used as it is, any other member is Duplex.from of it, and + * the pair reads and writes through _duplexify (duplexify.js). The + * members evaluate in source order. */ +function duplexFromPair( + lowerer: Lowerer, + literal: ts.ObjectLiteralExpression, + loc: SrcLoc, +): IrExpr { + const duplex: IrType = { kind: "object", className: "%Duplex" }; + const sides: { readable?: IrExpr; writable?: IrExpr } = {}; + for (const p of literal.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) + ? p.name.text + : null; + if (key !== "readable" && key !== "writable") { + lowerer.noLowering( + "Duplex.from over an object with members other than readable and writable", + p, + "pass { readable, writable } with streams or Duplex.from bodies", + ); + } + const value = ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + let valueT: IrType | null; + try { + valueT = lowerer.mapTypeOf(lowerer.typeOf(value)); + } catch { + valueT = null; + } + const valueSides = + valueT?.kind === "object" + ? streamSidesOf(lowerer, lowerer.classes.get(valueT.className)) + : null; + const wanted = key === "readable" ? "r" : "w"; + const direct = valueSides === "rw" || valueSides === wanted; + sides[key!] = direct + ? lowerStreamArg(lowerer, value, "compose") + : duplexFromBody(lowerer, value, loc); + } + const { readable, writable } = sides; + if (readable && writable) + return { + kind: "libCall", + fn: "stream.duplexifyPair", + args: [readable, writable], + type: duplex, + loc, + }; + if (readable) + return { kind: "libCall", fn: "stream.duplexifyReadable", args: [readable], type: duplex, loc }; + if (writable) + return { kind: "libCall", fn: "stream.duplexifyWritable", args: [writable], type: duplex, loc }; + return lowerer.noLowering( + "Duplex.from over an object with neither readable nor writable", + literal, + "pass { readable, writable } with streams or Duplex.from bodies", + ); +} + /** The checker type of a stage's string or Buffer chunks, or null for * other chunk types. */ function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts.Type | null { diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index bab07c458..734f9ee80 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -384,6 +384,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "writable.setDefaultEncoding", "stream.duplexFromGen", "stream.duplexFromDyn", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", "stream.destroy", "stream.destroyErr", "stream.iteratorClose", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1708ad01a..ad8b907da 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4069,6 +4069,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, + /** Duplex.from over a lone Readable or Writable, or a { readable, + * writable } pair of streams (duplexify.js's _duplexify). */ + "stream.duplexifyReadable": { argTypes: [null], result: VOID }, + "stream.duplexifyWritable": { argTypes: [null], result: VOID }, + "stream.duplexifyPair": { argTypes: [null, null], result: VOID }, + /** Duplex.from(promise): the promise and its box helper. */ + "stream.duplexFromPromise": { argTypes: [null, null], result: VOID }, + /** Duplex.from(asyncFunction): the stage, its source helper and the + * promise's box helper. */ + "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, /** Whether a readable.nextChunkDyn step answered the end (the done diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index da9cab1c3..84b767fab 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -221,6 +221,11 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.decodeStrings", "stream.duplexFromGen", "stream.duplexFromDyn", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", + "stream.duplexifyReadable", + "stream.duplexifyWritable", + "stream.duplexifyPair", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", @@ -277,6 +282,8 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "readable.fromIter", "stream.duplexFromGen", "stream.duplexFromDyn", + "stream.duplexFromPromise", + "stream.duplexFromAsyncFn", ]); /** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index a76784083..d08d0084f 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5499,6 +5499,11 @@ function validateFunction( e.fn === "stream.decodeStrings" || e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn" || + e.fn === "stream.duplexifyReadable" || + e.fn === "stream.duplexifyWritable" || + e.fn === "stream.duplexifyPair" || + e.fn === "stream.duplexFromPromise" || + e.fn === "stream.duplexFromAsyncFn" || e.fn === "stream.setConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" || @@ -5554,7 +5559,30 @@ function validateFunction( } return true; } - if (e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn") { + if (e.fn === "stream.duplexFromPromise") { + // Duplex.from(promise): (promise, box) → a Duplex. + if (e.args[0]?.type.kind !== "promise" || e.args[1]?.type.kind !== "func") + err(`libCall ${e.fn} takes a promise and its box helper`, e.loc); + if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream class`, e.loc); + return true; + } + if ( + e.fn === "stream.duplexifyReadable" || + e.fn === "stream.duplexifyWritable" || + e.fn === "stream.duplexifyPair" + ) { + // Duplex.from over streams: (r), (w) or (r, w) → a Duplex. + const want = e.fn === "stream.duplexifyPair" ? 2 : 1; + if (e.args.length !== want || !e.args.every((a) => isStreamObject(a.type))) + err(`libCall ${e.fn} takes ${want} stream argument(s)`, e.loc); + if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream class`, e.loc); + return true; + } + if ( + e.fn === "stream.duplexFromGen" || + e.fn === "stream.duplexFromDyn" || + e.fn === "stream.duplexFromAsyncFn" + ) { // Duplex.from: (stage, source, helpers...) or (body) → a Duplex. if (!isStreamObject(e.type)) { err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index bb6ccf874..169e89c2e 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v29"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v30"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v29"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v30"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json new file mode 100644 index 000000000..a3801c775 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7424-duplex-from-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7424-duplex-from-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json new file mode 100644 index 000000000..069ae4ef4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7425-duplex-from-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7425-duplex-from-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json new file mode 100644 index 000000000..e44b3f001 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7426-duplex-from-promise.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7426-duplex-from-promise.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json new file mode 100644 index 000000000..22d092c60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7427-duplex-from-async-function.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7427-duplex-from-async-function.mjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 71126f806..aa251dc34 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, + runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b0ec0005b..acf463824 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, + runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 317ee124b..5b7f8da0f 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v29(void) {} +void scr_runtime_abi_v30(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 6f3aae93a..57928cfb5 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v29(void); +void scr_runtime_abi_v30(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 4a53da4c0..992203eb4 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -1769,18 +1769,26 @@ ScrStream *scr_stream_from_dyn(ScrDyn *src, ScrDyn *opts) { } /* Duplex.from(body) over a checked-dynamic body (duplexify.js): a native - * Duplex answers itself, an iterable reads as Duplex.from does, and the - * other bodies Node accepts (a lone Readable or Writable, a function, a - * promise, a { readable, writable } pair) are not supported yet. */ + * Duplex answers itself, a lone native Readable or Writable is wrapped, a + * promise reads as its value, an iterable reads as Duplex.from does, and + * the other bodies Node accepts (a function, a thenable, a { readable, + * writable } pair) are not supported yet. */ static ScrStream *scr_stream_boxed(const ScrDyn *value); +static ScrStream *scr_dupx_from_boxed(ScrDyn *boxed); ScrStream *scr_stream_duplex_from_dyn(ScrDyn *body) { ScrStream *native = scr_stream_of_handle(body); /* +1 */ if (!native) native = scr_stream_boxed(body); /* +1 */ - if (native && native->st->has_r && native->st->has_w) return native; - bool unsupported = native != NULL; - if (native) scr_stream_release(native); - if (!unsupported && (body->kind == SCR_DYN_FUNC || body->kind == SCR_DYN_PROMISE)) unsupported = true; + if (native) { + if (native->st->has_r && native->st->has_w) return native; + /* _duplexify({ readable }) / _duplexify({ writable }) */ + ScrStream *d = native->st->has_r ? scr_stream_duplexify(native, NULL) + : scr_stream_duplexify(NULL, native); + scr_stream_release(native); + return d; + } + if (body->kind == SCR_DYN_PROMISE) return scr_dupx_from_boxed(scr_dyn_retain(body)); + bool unsupported = body->kind == SCR_DYN_FUNC; if (!unsupported && body->kind == SCR_DYN_OBJ) { static const char *const keys[] = {"then", "readable", "writable"}; for (size_t i = 0; i < 3 && !unsupported; i++) { @@ -4709,6 +4717,344 @@ ScrStream *scr_stream_compose_stages(double n, ScrStream **streams, double count return scr_stream_compose_impl((size_t)n, streams, &fs, abort); } +/* ── Duplex.from over a stream or a { readable, writable } pair ───────── + * duplexify.js's _duplexify({ readable, writable }): a Duplexify that + * writes to w (write() answering false waits for 'drain'; final ends w and + * waits for its 'finish') and reads r through 'readable' and read(), with + * each side's end of stream (eos) finishing the Duplex. The readable and + * writable object modes follow r's and w's. */ + +enum { SCR_DUPX_UNSET, SCR_DUPX_NULL, SCR_DUPX_WAIT }; + +typedef struct ScrDuplexify { + size_t rc; + ScrStream *r, *w; /* +1 or NULL */ + ScrStream *d; /* +1 until teardown */ + ScrClosure *drain_l, *finish_l, *readable_l, *end_l; /* +1, the listeners, or NULL */ + bool readable, writable; + bool ondrain, onfinish, onreadable; + int onclose; /* _duplexify's onclose: undefined, null, or d's destroy callback */ + struct ScrDuplexify *live_prev, *live_next; +} ScrDuplexify; + +static SCR_TL ScrDuplexify *scr_dupx_live = NULL; + +static void *scr_dupx_retain_v(void *p) { + ((ScrDuplexify *)p)->rc++; + return p; +} + +static void scr_dupx_release_v(void *p) { + ScrDuplexify *x = p; + if (--x->rc > 0) return; + if (x->r) scr_stream_release(x->r); + if (x->w) scr_stream_release(x->w); + free(x); +} + +static ScrClosure *scr_dupx_closure(void *fn, ScrDuplexify *x) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&scr_dupx_retain_v, &scr_dupx_release_v, NULL); + scr_box_set_ref(cl->caps[0], scr_dupx_retain_v(x)); + return cl; +} + +static void scr_dupx_on(ScrStream *s, const char *name, ScrClosure **slot, void *fn, ScrDuplexify *x) { + ScrStr *n = scr_str_new(name, strlen(name)); + *slot = scr_dupx_closure(fn, x); + scr_emitter_release( + scr_emitter_on((ScrEmitter *)s, n, scr_closure_retain(*slot), scr_ee_inv_fixed0, false, false)); + scr_str_release(n); +} + +/* Drop the Duplex and the listeners on r and w. */ +static void scr_dupx_teardown(ScrDuplexify *x) { + if (!x->d) return; + if (x->w) { + scr_compose_off(x->w, "drain", &x->drain_l); + scr_compose_off(x->w, "finish", &x->finish_l); + } + if (x->r) { + scr_compose_off(x->r, "readable", &x->readable_l); + scr_compose_off(x->r, "end", &x->end_l); + } + if (x->live_prev) x->live_prev->live_next = x->live_next; + else scr_dupx_live = x->live_next; + if (x->live_next) x->live_next->live_prev = x->live_prev; + x->live_prev = x->live_next = NULL; + ScrStream *d = x->d; + x->d = NULL; + scr_stream_release(d); + scr_dupx_release_v(x); /* the live list's reference */ +} + +static void scr_dupx_atexit(void) { + while (scr_dupx_live) scr_dupx_teardown(scr_dupx_live); +} + +/* d's destroy callback: callback(err), then the context lets go. */ +static void scr_dupx_destroyed(ScrDuplexify *x, ScrError *err) { + ScrStream *d = scr_stream_retain(x->d); + scr_dupx_teardown(x); + scr_stream_destroy_done(d, err ? scr_error_retain(err) : NULL); + scr_stream_release(d); +} + +/* onfinished(err): the pending destroy callback, else d.destroy(err) */ +static void scr_dupx_onfinished(ScrDuplexify *x, ScrError *err) { + int onclose = x->onclose; + x->onclose = SCR_DUPX_NULL; + if (!x->d) return; + if (onclose == SCR_DUPX_WAIT) { + scr_dupx_destroyed(x, err); + } else if (err) { + ScrStream *d = scr_stream_retain(x->d); + scr_stream_release(scr_stream_destroy(d, err)); + scr_stream_release(d); + } +} + +/* eos(w): writable = false; an error destroys r */ +static void scr_dupx_w_eos(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->writable = false; + if (err && x->r) scr_stream_release(scr_stream_destroy(x->r, err)); + scr_dupx_onfinished(x, err); + scr_dupx_release_v(x); +} + +/* eos(r): readable = false; an error destroys w */ +static void scr_dupx_r_eos(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->readable = false; + if (err && x->w) scr_stream_release(scr_stream_destroy(x->w, err)); + scr_dupx_onfinished(x, err); + scr_dupx_release_v(x); +} + +/* d._write: w.write(chunk) ? callback() : (ondrain = callback) */ +static void scr_dupx_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + bool ok = scr_stream_writes_values(s) ? scr_stream_write_dyn(x->w, (ScrDyn *)chunk, NULL) + : scr_stream_write(x->w, chunk, NULL); + if (ok) { + if (!scr_exc_pending()) scr_stream_write_done(s, NULL); + } else { + x->ondrain = true; + } + scr_dupx_release_v(x); +} + +/* d._final: w.end(); onfinish = callback */ +static void scr_dupx_final_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + ScrStream *r = scr_stream_end(x->w, NULL, NULL, NULL); + if (r) scr_stream_release(r); + x->onfinish = true; + scr_dupx_release_v(x); +} + +static void scr_dupx_drain_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->ondrain && x->d) { + x->ondrain = false; + scr_stream_write_done(x->d, NULL); + } + scr_dupx_release_v(x); +} + +static void scr_dupx_finish_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->onfinish && x->d) { + x->onfinish = false; + scr_stream_final_done(x->d, NULL); + } + scr_dupx_release_v(x); +} + +/* d._read: push r.read() until it answers null (then wait for 'readable') + * or d is full */ +static void scr_dupx_pull(ScrDuplexify *x) { + ScrStream *d = x->d; + while (d && x->r) { + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *chunk = scr_stream_read_n_kind(x->r, -1, &kind); + if (scr_exc_pending()) return; + if (!chunk) { + x->onreadable = true; + return; + } + bool more; + if (d->st->r.object_entries) { + more = scr_stream_add_entry(d, chunk, kind) > 0; + } else if (kind == SCR_ENTRY_STR) { + more = scr_stream_push_str(d, chunk); + scr_str_release(chunk); + } else if (kind == SCR_ENTRY_DYN) { + more = scr_stream_push_dyn(d, chunk); + scr_dyn_release(chunk); + } else { + more = scr_stream_push(d, chunk); + scr_bytes_release(chunk); + } + if (!more || scr_exc_pending()) return; + } +} + +static void scr_dupx_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)s; + (void)size; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + x->onreadable = false; + scr_dupx_pull(x); + scr_dupx_release_v(x); +} + +static void scr_dupx_readable_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->onreadable && x->d) { + x->onreadable = false; + scr_dupx_pull(x); + } + scr_dupx_release_v(x); +} + +static void scr_dupx_end_l(ScrClosure *cb) { + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + if (x->d) scr_stream_push_null(x->d); + scr_dupx_release_v(x); +} + +/* d._destroy */ +static void scr_dupx_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + ScrDuplexify *x = scr_box_get_ref(cb->caps[0]); + ScrError *e = err ? scr_error_retain(err) : NULL; + if (!e && x->onclose != SCR_DUPX_NULL) e = scr_compose_abort_error(); + x->onreadable = x->ondrain = x->onfinish = false; + if (x->onclose == SCR_DUPX_NULL) { + if (x->d) scr_dupx_destroyed(x, e); + } else { + x->onclose = SCR_DUPX_WAIT; + if (x->w && !x->w->st->destroyed) scr_stream_release(scr_stream_destroy(x->w, e)); + if (x->r && !x->r->st->destroyed) scr_stream_release(scr_stream_destroy(x->r, e)); + } + if (e) scr_error_release(e); + scr_dupx_release_v(x); +} + +/* _duplexify({ readable: r, writable: w }): r and w borrowed, either NULL. + * +1. */ +ScrStream *scr_stream_duplexify(ScrStream *r, ScrStream *w) { + ScrDuplexify *x = calloc(1, sizeof *x); + if (!x) scr_stream_oom(); + x->rc = 1; + x->r = r ? scr_stream_retain(r) : NULL; + x->w = w ? scr_stream_retain(w) : NULL; + x->onclose = SCR_DUPX_UNSET; + x->readable = r && scr_stream_prop(r, "readable") != 0; + x->writable = w && scr_stream_prop(w, "writable") != 0; + /* new Duplexify({ readableObjectMode, writableObjectMode, readable, writable }) */ + bool r_object = r && r->st->has_r && r->st->r.object_entries; + bool w_object = w && w->st->has_w && w->st->w.object_mode; + ScrStream *d = scr_stream_new_duplex( + r_object ? 16 : -1, w_object ? 16 : -1, true, true, true, x->readable, x->writable, + x->readable ? scr_dupx_closure((void *)&scr_dupx_read_inv, x) : NULL, + x->readable ? &scr_dupx_read_inv : NULL, + x->writable ? scr_dupx_closure((void *)&scr_dupx_write_inv, x) : NULL, + x->writable ? &scr_dupx_write_inv : NULL, + x->writable ? scr_dupx_closure((void *)&scr_dupx_final_inv, x) : NULL, + x->writable ? &scr_dupx_final_inv : NULL, + scr_dupx_closure((void *)&scr_dupx_destroy_inv, x), &scr_dupx_destroy_inv); + if (r_object) d->st->r.object_entries = true; + if (w_object) scr_stream_set_object_mode(d, false, true); + d->cls = "Duplexify"; + x->d = scr_stream_retain(d); + if (x->writable) { + scr_closure_release( + scr_stream_finished(w, scr_dupx_closure((void *)&scr_dupx_w_eos, x), &scr_dupx_w_eos)); + scr_dupx_on(w, "drain", &x->drain_l, (void *)&scr_dupx_drain_l, x); + scr_dupx_on(w, "finish", &x->finish_l, (void *)&scr_dupx_finish_l, x); + } + if (x->readable) { + scr_closure_release( + scr_stream_finished(r, scr_dupx_closure((void *)&scr_dupx_r_eos, x), &scr_dupx_r_eos)); + scr_dupx_on(r, "readable", &x->readable_l, (void *)&scr_dupx_readable_l, x); + scr_dupx_on(r, "end", &x->end_l, (void *)&scr_dupx_end_l, x); + } + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(scr_dupx_atexit); + } + x->live_next = scr_dupx_live; + if (scr_dupx_live) scr_dupx_live->live_prev = x; + scr_dupx_live = x; /* the reference x was created with */ + return d; +} + +/* Duplex.from(promise) (duplexify.js): a Duplexify({ objectMode: true, + * writable: false, read() {} }) that pushes the promise's value unless it + * is nullish, then ends; a rejection destroys it with the reason. */ +typedef struct { + ScrStream *d; /* +1 */ + ScrDyn *boxed; /* +1, the promise boxed */ +} ScrDupxPromise; + +static void scr_dupx_noop_read(ScrClosure *cb, ScrStream *s, double size) { + (void)cb; + (void)s; + (void)size; +} + +static void scr_dupx_promise_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + ScrDupxPromise p = *(ScrDupxPromise *)arg; + free(arg); + /* the boxed promise's payload is a dyn value (NULL: undefined) */ + ScrDyn *v = scr_await_ref(p.boxed->v.promise); + if (!scr_exc_pending()) { + if (v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) scr_stream_push_object(p.d, v); + if (!scr_exc_pending()) scr_stream_push_null(p.d); + if (scr_exc_pending()) scr_exc_clear(); + if (v) scr_dyn_release(v); + } else { + ScrError *err = scr_pump_caught(); + scr_stream_release(scr_stream_destroy(p.d, err)); + if (scr_exc_pending()) scr_exc_clear(); + scr_error_release(err); + } + scr_stream_release(p.d); + scr_dyn_release(p.boxed); +} + +/* The readable Duplexify over a boxed promise (moves boxed). +1. */ +static ScrStream *scr_dupx_from_boxed(ScrDyn *boxed) { + ScrClosure *read = scr_closure_new((void *)&scr_dupx_noop_read, 0); + ScrStream *d = scr_stream_new_duplex(16, 16, true, true, true, true, false, read, + &scr_dupx_noop_read, NULL, NULL, NULL, NULL, NULL, NULL); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + ScrDupxPromise *ctx = malloc(sizeof *ctx); + if (!ctx) scr_stream_oom(); + ctx->d = scr_stream_retain(d); + ctx->boxed = boxed; + scr_promise_release(scr_async_spawn_after(ctx->boxed->v.promise, &scr_dupx_promise_entry, ctx)); + return d; +} + +/* p and box borrowed (box: the compiler's promise → dyn promise helper). +1. */ +ScrStream *scr_stream_duplex_from_promise(ScrPromise *p, ScrClosure *box) { + return scr_dupx_from_boxed(((ScrPipeBoxFn)box->fn)(box, scr_promise_retain(p))); +} + +ScrStream *scr_stream_duplexify_readable(ScrStream *r) { return scr_stream_duplexify(r, NULL); } +ScrStream *scr_stream_duplexify_writable(ScrStream *w) { return scr_stream_duplexify(NULL, w); } +ScrStream *scr_stream_duplexify_pair(ScrStream *r, ScrStream *w) { return scr_stream_duplexify(r, w); } + /* ── node:stream/promises ───────────────────────────────────────────── * The promise forms ride the callback machinery above with a settling * watcher: caps[0] boxes the pending promise; the terminal status diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 623f60465..d276700c8 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -545,6 +545,20 @@ void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritev ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, ScrClosure *read, ScrClosure *ret, ScrClosure *thr); ScrStream *scr_stream_duplex_from_dyn(struct ScrDyn *body); +/* Duplex.from over an async function (the compiled stage takes the source + * generator and answers its promise; source and the promise's box helper + * borrowed): a writable Duplexify whose final waits for the promise. +1. */ +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box); +/* Duplex.from over a Readable, a Writable, or a { readable, writable } + * pair (duplexify.js's _duplexify): a Duplexify that reads r and writes w + * (either NULL in scr_stream_duplexify; streams borrowed). +1. */ +ScrStream *scr_stream_duplexify(ScrStream *r, ScrStream *w); +ScrStream *scr_stream_duplexify_readable(ScrStream *r); +ScrStream *scr_stream_duplexify_writable(ScrStream *w); +ScrStream *scr_stream_duplexify_pair(ScrStream *r, ScrStream *w); +/* Duplex.from(promise): a readable Duplexify of the promise's value. p and + * box (the compiler's promise → dyn promise helper) borrowed. +1. */ +ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box); /* decodeStrings (false keeps string writes as strings), the writable * defaultEncoding option (a known encoding, borrowed), and * setDefaultEncoding (validated; answers the stream +1). The chaining diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index 1db90bc5a..75ce416ac 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -2042,7 +2042,7 @@ ScrStream *scr_stream_from_async_iter(ScrDyn *iter, double flags, double hwm) { * (the chunk, or the done marker), and ack() is fromAsyncGen's * `process.nextTick(cb)` followed by a fresh deferred. */ -enum { CHAN_NONE, CHAN_WRITE, CHAN_FINAL, CHAN_ABORT }; +enum { CHAN_NONE, CHAN_WRITE, CHAN_FINAL, CHAN_ABORT, CHAN_FINAL_ASYNC }; typedef struct OpChan OpChan; struct OpChan { @@ -2052,6 +2052,12 @@ struct OpChan { ScrPromise *resolve; /* +1: the deferred a write or the final settles, NULL once settled */ int settled; /* CHAN_*: what settled the deferred, for ack() */ bool aborted; /* destroy ran (fromAsyncGen's ac.abort()) */ + /* An async function body: its promise's then() (duplexify.js) settles + * `promise` there; the final waits for it. */ + bool finalized; /* final() has started */ + bool body_settled; /* the then() callbacks ran */ + bool final_waiting; /* the final's callback awaits the settlement */ + ScrError *body_err; /* +1: what awaiting the then() result throws, or NULL */ OpChan *live_prev, *live_next; }; @@ -2059,6 +2065,7 @@ static SCR_TL OpChan *op_chan_live = NULL; static void op_chan_drop(OpObj *o) { OpChan *c = (OpChan *)o; + if (c->body_err) scr_error_release(c->body_err); if (c->d) scr_stream_release(c->d); if (c->promise) scr_promise_release(c->promise); if (c->resolve) scr_promise_release(c->resolve); @@ -2136,10 +2143,49 @@ static ScrError *op_chan_abort_error(void) { /* After the source's await: process.nextTick(cb), then (unless done) the * aborted check and a fresh deferred. */ +static void op_chan_final_async_wait(OpChan *c); + +/* The final's completion after `await promise`: process.nextTick(cb, err) */ +static void op_chan_final_async_done(ScrClosure *job) { + OpChan *c = op_chan_of(job); + if (c->d) { + ScrError *err = c->body_err ? scr_error_retain(c->body_err) : NULL; + ScrStream *d = scr_stream_retain(c->d); + scr_stream_final_done(d, err); + scr_stream_release(d); + } + op_unref(c); +} + +/* `await promise` resumed: the completion follows on the next tick */ +static void op_chan_final_async_resume(ScrClosure *job) { + OpChan *c = op_chan_of(job); + scr_next_tick(op_chan_closure((void *)&op_chan_final_async_done, c)); + op_unref(c); +} + +/* The final's async callback (run by the source's nextTick): it awaits + * the then() result, which may still be pending. */ +static void op_chan_final_async_tick(ScrClosure *job) { + OpChan *c = op_chan_of(job); + op_chan_final_async_wait(c); + op_unref(c); +} + +static void op_chan_final_async_wait(OpChan *c) { + if (c->body_settled) scr_queue_microtask(op_chan_closure((void *)&op_chan_final_async_resume, c)); + else c->final_waiting = true; +} + static void op_chan_ack_inv(ScrClosure *cb) { OpChan *c = op_chan_of(cb); int settled = c->settled; c->settled = CHAN_NONE; + if (settled == CHAN_FINAL_ASYNC) { + scr_next_tick(op_chan_closure((void *)&op_chan_final_async_tick, c)); + op_unref(c); + return; + } if (c->d && (settled == CHAN_WRITE || settled == CHAN_FINAL)) { ScrClosure *tick = scr_closure_new((void *)&op_chan_done_tick, 2); tick->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); @@ -2261,6 +2307,126 @@ ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, Scr return d; } +/* ── Duplex.from(asyncFunction) ── duplexify.js: the function's promise + * must settle to a nullish value (else ERR_INVALID_RETURN_VALUE when the + * final awaits it), a return before the input was consumed destroys the + * Duplex, a rejection destroys it with the reason, and the Duplexify is + * writable only, its final waiting for the promise. */ + +typedef ScrPromise *(*OpAsyncStageFn)(ScrClosure *, ScrGen *); +typedef ScrDyn *(*OpBoxFn)(ScrClosure *, ScrPromise *); + +typedef struct { + OpChan *c; /* +1 */ + ScrDyn *boxed; /* +1, the function's promise boxed */ +} OpAsyncBody; + +static void op_async_body_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + OpAsyncBody b = *(OpAsyncBody *)arg; + free(arg); + OpChan *c = b.c; + ScrDyn *v = scr_await_ref(b.boxed->v.promise); /* NULL: undefined */ + ScrStream *d = c->d ? scr_stream_retain(c->d) : NULL; + if (!scr_exc_pending()) { + if (v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF) { + /* the then() callback throws: awaiting its result rejects */ + char detail[160]; + const char *got = scr_dyn_specific_type(v, detail, sizeof detail); + char msg[320]; + int n = snprintf(msg, sizeof msg, + "Expected nully to be returned from the \"body\" function but got %s.", got); + c->body_err = op_coded_error(SCR_ERR_TYPE, "ERR_INVALID_RETURN_VALUE", msg); + (void)n; + } else if (!c->finalized && d) { + /* returned without consuming the input: destroyer(d) */ + ScrError *e = op_chan_abort_error(); + scr_stream_release(scr_stream_destroy(d, e)); + scr_error_release(e); + } + if (v) scr_dyn_release(v); + } else { + ScrCaught *caught = scr_exc_take(); + ScrError *err = op_caught_error(caught); + scr_caught_release(caught); + if (d) scr_stream_release(scr_stream_destroy(d, err)); + scr_error_release(err); + } + if (scr_exc_pending()) scr_exc_clear(); + if (d) scr_stream_release(d); + c->body_settled = true; + if (c->final_waiting) { + c->final_waiting = false; + op_chan_final_async_wait(c); + } + op_unref(c); + scr_dyn_release(b.boxed); +} + +/* Duplexify's final(cb) for an async function body: finalized, then + * fromAsyncGen's final with the awaiting callback */ +static void op_chan_final_async_inv(ScrClosure *cb, ScrStream *s) { + (void)s; + OpChan *c = op_chan_of(cb); + c->finalized = true; + op_chan_settle(c, scr_stream_from_done(), CHAN_FINAL_ASYNC); + op_unref(c); +} + +/* fromAsyncGen's destroy alone (no from() iterator to close) */ +static void op_chan_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + OpChan *c = op_chan_of(cb); + c->aborted = true; + if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); + op_chan_retire(c); + op_unref(c); + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +/* Duplex.from(fn) for an async function: the compiled stage (borrowed) + * takes the source generator and answers its promise; source and box + * (promise → dyn promise) borrowed. +1, or NULL with the function's throw + * pending. */ +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box) { + op_sentinels_init(); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(op_chan_atexit); + } + OpChan *c = op_alloc(sizeof *c); + c->base.rc = 1; + c->base.drop = &op_chan_drop; + op_chan_defer(c); + ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), + op_chan_closure((void *)&op_chan_ack_inv, c)); + ScrPromise *p = scr_exc_pending() ? NULL : ((OpAsyncStageFn)stage->fn)(stage, src); + if (scr_exc_pending()) { + if (p) scr_promise_release(p); + op_unref(c); + return NULL; + } + /* Duplexify({ objectMode: true, readable: false, write, final, destroy }) */ + ScrStream *d = scr_stream_new_duplex(16, 16, true, true, true, false, true, NULL, NULL, + op_chan_closure((void *)&op_chan_write_inv, c), &op_chan_write_inv, + op_chan_closure((void *)&op_chan_final_async_inv, c), + &op_chan_final_async_inv, + op_chan_closure((void *)&op_chan_destroy_inv, c), &op_chan_destroy_inv); + scr_stream_set_object_mode(d, true, true); + d->cls = "Duplexify"; + c->d = scr_stream_retain(d); + c->live_next = op_chan_live; + if (op_chan_live) op_chan_live->live_prev = c; + op_chan_live = c; /* the reference c was created with */ + /* value.then(onFulfilled, onRejected) */ + OpAsyncBody *b = malloc(sizeof *b); + if (!b) op_oom(); + b->c = op_ref(c); + b->boxed = ((OpBoxFn)box->fn)(box, p); /* p moves */ + scr_promise_release(scr_async_spawn_after(b->boxed->v.promise, &op_async_body_entry, b)); + return d; +} + /* ── the promise operators ─────────────────────────────────────────── */ enum { PK_DYN, PK_F64, PK_BOOL, PK_STR, PK_VOID }; diff --git a/tests/corpus/7424-duplex-from-streams.mjs b/tests/corpus/7424-duplex-from-streams.mjs new file mode 100644 index 000000000..5a9bdc01a --- /dev/null +++ b/tests/corpus/7424-duplex-from-streams.mjs @@ -0,0 +1,39 @@ +// Duplex.from over a lone Readable, a lone Writable and a { readable, +// writable } pair, as duplexify.js's _duplexify wraps them: the Duplex +// reads through the Readable and writes through the Writable, takes each +// side's object mode from the stream behind it, and closes once both ends +// finish; destroying it without an error destroys the streams behind it. +import { Duplex, PassThrough, Readable, Writable } from "node:stream"; +const log = []; +const r = Readable.from(["a", "b"]); +const dr = Duplex.from(r); +log.push(`dr ${dr.readable} ${dr.writable} ${dr.readableObjectMode} ${dr.writableObjectMode}`); +dr.on("end", () => log.push("dr end")); +dr.on("close", () => log.push("dr close")); +for await (const c of dr) log.push(`dr ${c}`); +const got = []; +const w = new Writable({ write(c, e, cb) { got.push(String(c)); log.push(`w write ${c}`); cb(); } }); +const dw = Duplex.from(w); +log.push(`dw ${dw.readable} ${dw.writable} ${dw.writableObjectMode}`); +dw.on("finish", () => log.push("dw finish")); +dw.on("close", () => log.push("dw close")); +dw.write("x", () => log.push("dw cb x")); +dw.end("y", () => log.push("dw end cb")); +await new Promise((res) => dw.on("close", res)); +const inner = new PassThrough({ objectMode: true }); +const out = new PassThrough({ objectMode: true }); +const pair = Duplex.from({ readable: out, writable: inner }); +log.push(`pair ${pair.readable} ${pair.writable} ${pair.readableObjectMode} ${pair.writableObjectMode}`); +inner.on("data", (v) => { log.push(`inner ${JSON.stringify(v)}`); out.write({ echo: v }); }); +inner.on("finish", () => { log.push("inner finish"); out.end(); }); +pair.on("data", (v) => log.push(`pair data ${JSON.stringify(v)}`)); +pair.write({ n: 1 }); +pair.end({ n: 2 }); +await new Promise((res) => pair.on("close", res)); +log.push("pair closed"); +const killed = Duplex.from(new PassThrough()); +killed.on("error", (e) => log.push(`killed error ${e.name}`)); +killed.on("close", () => log.push(`killed close ${killed.destroyed}`)); +killed.destroy(); +await new Promise((res) => setTimeout(res, 5)); +console.log(log.join("\n")); diff --git a/tests/corpus/7425-duplex-from-streams.ts b/tests/corpus/7425-duplex-from-streams.ts new file mode 100644 index 000000000..a9d85b797 --- /dev/null +++ b/tests/corpus/7425-duplex-from-streams.ts @@ -0,0 +1,72 @@ +// Duplex.from over streams in typed code: a Readable that fails destroys +// the Duplex with its error, a slow Writable's callbacks and the end +// callback follow its pace, a pair whose writable member is an async +// generator function's Duplex, compose() with an async function as its +// last stage, and a Duplex over a promise. +import { Duplex, PassThrough, Readable, Writable, compose } from "node:stream"; +const log: string[] = []; +async function main(): Promise { + const failing = new Readable({ read(): void { this.destroy(new Error("source failed")); } }); + const df = Duplex.from(failing); + df.on("error", (e: Error) => log.push(`df error ${e.message}`)); + df.on("close", () => log.push(`df close ${df.destroyed}`)); + df.resume(); + await new Promise((res) => df.on("close", res)); + const slow = new Writable({ + highWaterMark: 2, + write(c: Buffer, _e: string, cb: (err?: Error | null) => void): void { + log.push(`slow write ${c.toString()}`); + setTimeout(cb, 2); + }, + }); + const ds = Duplex.from(slow); + log.push(`write1 ${ds.write("aa", () => log.push("cb aa"))}`); + log.push(`write2 ${ds.write("bb", () => log.push("cb bb"))}`); + ds.on("drain", () => log.push("ds drain")); + ds.end(() => log.push("ds end cb")); + await new Promise((res) => ds.on("finish", res)); + log.push("ds finished"); + const upper = new PassThrough(); + const dp = Duplex.from({ + readable: Readable.from(["r1", "r2"]), + writable: async function* (source: AsyncIterable): AsyncGenerator { + for await (const c of source) log.push(`gen got ${String(c)}`); + yield "unused"; + }, + }); + dp.on("data", (c: unknown) => log.push(`dp data ${String(c)}`)); + dp.write("w1"); + dp.end(); + await new Promise((res) => dp.on("end", res)); + upper.end(); + await more(); + for (const line of log) console.log(line); +} +async function more(): Promise { + const sink = compose(new PassThrough({ objectMode: true }), async function (source: AsyncIterable): Promise { + for await (const c of source) log.push(`sink ${String(c)}`); + }); + log.push(`sink r=${sink.readable} w=${sink.writable}`); + sink.write("p"); + sink.end("q"); + await new Promise((res) => sink.on("finish", res)); + log.push("sink finished"); + const fromReadable: Duplex = Duplex.from(Readable.from([1, 2])); + const values: unknown[] = []; + for await (const v of fromReadable) values.push(v); + log.push(`readable ${JSON.stringify(values)}`); + const written: string[] = []; + const target = new Writable({ + write(c: Buffer, _e: string, cb: (err?: Error | null) => void): void { + written.push(c.toString()); + cb(); + }, + }); + const fromWritable: Duplex = Duplex.from(target); + fromWritable.end("only"); + await new Promise((res) => fromWritable.on("finish", res)); + log.push(`writable ${written.join(",")}`); + const value: Duplex = Duplex.from(Promise.resolve("resolved")); + for await (const v of value) log.push(`promise ${String(v)}`); +} +void main(); diff --git a/tests/corpus/7426-duplex-from-promise.mjs b/tests/corpus/7426-duplex-from-promise.mjs new file mode 100644 index 000000000..46d19586b --- /dev/null +++ b/tests/corpus/7426-duplex-from-promise.mjs @@ -0,0 +1,27 @@ +// Duplex.from over a promise: a Duplex that is not writable and reads the +// promise's value once it settles (nothing for null), then ends; a +// rejection destroys it with the reason. The events interleave with +// microtasks and nextTick callbacks as in Node. +import { Duplex } from "node:stream"; +const log = []; +const tick = (n) => { if (n < 5) { log.push(`tick ${n}`); process.nextTick(() => tick(n + 1)); } }; +const later = (v, ms) => new Promise((res) => setTimeout(() => res(v), ms)); +const a = Duplex.from(Promise.resolve({ answer: 42 })); +log.push(`a ${a.readable} ${a.writable} ${a.readableObjectMode}`); +a.on("data", (v) => log.push(`a data ${JSON.stringify(v)}`)); +a.on("end", () => log.push("a end")); +a.on("close", () => log.push("a close")); +Promise.resolve().then(() => log.push("microtask 1")).then(() => log.push("microtask 2")); +tick(0); +await new Promise((res) => a.on("close", res)); +const b = Duplex.from(later(null, 1)); +b.on("data", () => log.push("b data")); +b.on("end", () => log.push("b end")); +await new Promise((res) => b.on("close", res)); +const c = Duplex.from(Promise.reject(new Error("nope"))); +c.on("error", (e) => log.push(`c error ${e.message}`)); +c.on("close", () => log.push(`c close ${c.destroyed}`)); +await new Promise((res) => c.on("close", res)); +const d = Duplex.from(later("text", 1)); +for await (const v of d) log.push(`d ${v}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7427-duplex-from-async-function.mjs b/tests/corpus/7427-duplex-from-async-function.mjs new file mode 100644 index 000000000..66ce35f9a --- /dev/null +++ b/tests/corpus/7427-duplex-from-async-function.mjs @@ -0,0 +1,40 @@ +// Duplex.from over an async function: a Duplex that is only writable, whose +// writes reach the function's source and whose end waits for the +// function's promise. A value other than null or undefined is +// ERR_INVALID_RETURN_VALUE, a return before the input is consumed aborts +// the Duplex, and a throw destroys it. +import { Duplex } from "node:stream"; +const log = []; +const sink = Duplex.from(async function (source) { + for await (const c of source) log.push(`sink got ${c}`); + log.push("sink done"); +}); +log.push(`sink ${sink.readable} ${sink.writable} ${sink.writableObjectMode}`); +sink.on("finish", () => log.push("sink finish")); +sink.on("close", () => log.push("sink close")); +sink.write("a", () => log.push("cb a")); +sink.end("b", () => log.push("end cb")); +await new Promise((res) => sink.on("close", res)); +const bad = Duplex.from(async function (source) { + for await (const c of source) log.push(`bad got ${c}`); + return 5; +}); +bad.on("error", (e) => log.push(`bad error ${e.code} ${e.message}`)); +bad.on("close", () => log.push("bad close")); +bad.end("x"); +await new Promise((res) => bad.on("close", res)); +const early = Duplex.from(async function (source) { + for await (const c of source) { log.push(`early got ${c}`); break; } +}); +early.on("error", (e) => log.push(`early error ${e.name}`)); +early.on("close", () => log.push(`early close ${early.destroyed}`)); +early.write("1"); +early.write("2"); +await new Promise((res) => early.on("close", res)); +const failing = Duplex.from(async function (source) { + for await (const c of source) throw new Error("fail " + c); +}); +failing.on("error", (e) => log.push(`failing error ${e.message}`)); +failing.write("z"); +await new Promise((res) => failing.on("close", res)); +console.log(log.join("\n")); diff --git a/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json b/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json new file mode 100644 index 000000000..14a2052b9 --- /dev/null +++ b/tests/harness/corpus-costs/7424-duplex-from-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2607, "san": 3440 } diff --git a/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json b/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json new file mode 100644 index 000000000..9dde8d70e --- /dev/null +++ b/tests/harness/corpus-costs/7425-duplex-from-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 2009, "san": 2743 } diff --git a/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json b/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json new file mode 100644 index 000000000..9195268ab --- /dev/null +++ b/tests/harness/corpus-costs/7426-duplex-from-promise.mjs.json @@ -0,0 +1 @@ +{ "plain": 1877, "san": 2357 } diff --git a/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json b/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json new file mode 100644 index 000000000..c572d9930 --- /dev/null +++ b/tests/harness/corpus-costs/7427-duplex-from-async-function.mjs.json @@ -0,0 +1 @@ +{ "plain": 1811, "san": 2417 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index f143419dc..81ff5b293 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" }, + runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 18c29009379adef0254dafcc7f51f81c2e4aa9b6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 14:20:01 -0500 Subject: [PATCH 042/169] Read web ReadableStreams as async iterable stream sources --- .../builtin-apis/streams-and-readline.mdx | 3 +- .../src/frontend/lowering/lower-stream.ts | 282 +++++++++++++++++- .../corpus/7428-web-stream-sources.mjs.json | 6 + .../corpus/7429-web-stream-sources.ts.json | 6 + tests/corpus/7428-web-stream-sources.mjs | 41 +++ tests/corpus/7429-web-stream-sources.ts | 42 +++ .../7428-web-stream-sources.mjs.json | 1 + .../7429-web-stream-sources.ts.json | 1 + 8 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json create mode 100644 tests/corpus/7428-web-stream-sources.mjs create mode 100644 tests/corpus/7429-web-stream-sources.ts create mode 100644 tests/harness/corpus-costs/7428-web-stream-sources.mjs.json create mode 100644 tests/harness/corpus-costs/7429-web-stream-sources.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index cfd52e054..eab139773 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -25,7 +25,8 @@ It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`. A web stream and a body function that declares the options argument are unsupported in `Duplex.from`. `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. - Web streams as compose stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. + A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`; a web stream in a later compose stage, and `Readable.fromWeb` and `Readable.toWeb`, are unsupported. + The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 7c0c2b46c..a7f1469a9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1864,6 +1864,42 @@ function lowerFromSource( // SCR_FROM_DUPLEX const duplexFlag = duplex ? 32 : 0; const js = isJsSourceFile(ts.getSourceFileOfNode(call)); + // A web ReadableStream is async iterable: from.js reads its iterator. + // (Duplex.from takes Readable.fromWeb of it instead.) + const webElem = srcNode ? staticReadableStreamElementType(lowerer, srcNode) : null; + if (srcNode && webElem !== null) { + if (duplex) { + lowerer.noLowering( + "Duplex.from over a web ReadableStream", + srcNode, + "Readable.from(stream) reads a web ReadableStream", + ); + } + const source = lowerWebStreamSource(lowerer, srcNode, webElem, loc); + const helpers = source ? fromGenHelpers(lowerer, source.out, loc) : null; + const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; + if (!source || !helpers || !opts || !fromGenChunk(lowerer, webElem)) { + lowerer.noLowering( + `Readable.from over a web ReadableStream of '${lowerer.fmt(webElem)}'`, + srcNode, + ); + } + const hwm = opts!.hwm ? lowerer.lowerExprExpecting(opts!.hwm, F64) : numLit(0, loc); + const gen: IrExpr = { + kind: "callValue", + callee: source!.closure, + args: [], + type: source!.out, + loc, + }; + return { + kind: "libCall", + fn: "readable.fromAsyncGen", + args: [gen, ...helpers!, numLit(opts!.flags, loc), hwm], + type, + loc, + }; + } const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null; const typedElem = (t: IrType | null | undefined): "string" | "bytes" | null => t?.kind === "string" ? "string" : t?.kind === "bytes" && t.elem === "u8" ? "bytes" : null; @@ -3120,6 +3156,8 @@ type SyncGenType = IrType & { kind: "generator" }; * array, a string or a synchronous generator object), or null. */ function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | null { if (pipelineStageFunction(lowerer, node) !== null) return null; + const webElem = staticReadableStreamElementType(lowerer, node); + if (webElem !== null) return webElem; let t: IrType | null; try { t = lowerer.mapTypeOf(lowerer.typeOf(node)); @@ -3139,12 +3177,245 @@ function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | nul * through an async-from-sync iterator, and an async generator that * yields the same elements resumes its consumer on the same ticks. Null * when the source has no such lowering. */ +/** A web ReadableStream source, as an async generator closure over its + * reader: each pass awaits reader.read() and yields the chunk, as the + * stream's async iterator does; leaving early cancels the stream (its + * iterator's return()), and the reader's lock is released either way. */ +function lowerWebStreamSource( + lowerer: Lowerer, + node: ts.Expression, + elemT: IrType, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + const resultT = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!resultT) return null; + const valueField = lowerer.shapes.get(resultT.shapeId)?.fields.find((f) => f.name === "value"); + if (!valueField) return null; + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + const genResultT = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!genResultT) return null; + const value = lowerer.lowerExprExpecting(node, DYN); + const key = `webStreamSource:${typeKey(elemT)}`; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.webStreamSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", DYN, false); + const readerId = frame.local("reader", DYN, false); + const activeId = frame.local("active", BOOL, true); + const promiseT: IrType = { kind: "promise", inner: resultT }; + const pId = frame.local("p", promiseT, false); + const rId = frame.local("r", resultT, false); + const reader = (): IrExpr => varRef(readerId, DYN, loc); + const r = (): IrExpr => varRef(rId, resultT, loc); + const setActive = (on: boolean): IrStmt => ({ + kind: "assign", + localId: activeId, + value: { kind: "boolLit", value: on, type: BOOL, loc }, + loc, + }); + const extracted = extractIteratorValue( + lowerer, + elemT, + valueField.type, + { + kind: "recordGet", + obj: r(), + shapeId: resultT.shapeId, + field: "value", + type: valueField.type, + loc, + }, + loc, + ); + if (!extracted) return null; + const invoke = (method: string, args: IrExpr[], calleeName: string): IrExpr => ({ + kind: "dynInvoke", + recv: reader(), + method, + calleeName, + args, + type: DYN, + loc, + }); + const body: IrStmt[] = [ + { + kind: "varDecl", + localId: readerId, + init: { + kind: "dynInvoke", + recv: varRef(srcId, DYN, loc), + method: "getReader", + calleeName: "ReadableStream async iterator", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: activeId, + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + setActive(false), + { + kind: "varDecl", + localId: pId, + init: { + kind: "libCall", + fn: "fetch.readerRead", + args: [reader()], + type: promiseT, + loc, + }, + loc, + }, + { + kind: "varDecl", + localId: rId, + init: { kind: "awaitExpr", value: varRef(pId, promiseT, loc), type: resultT, loc }, + loc, + }, + { + kind: "if", + cond: { + kind: "recordGet", + obj: r(), + shapeId: resultT.shapeId, + field: "done", + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setActive(true), + { + kind: "exprStmt", + expr: { kind: "yieldExpr", value: extracted, type: VOID, loc }, + loc, + }, + ], + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "tryCatch", + tryBody: [ + { + kind: "if", + cond: varRef(activeId, BOOL, loc), + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "async.awaitDyn", + args: [ + invoke( + "cancel", + [dynUndefinedExpr(loc)], + "ReadableStream async iterator return", + ), + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "exprStmt", + expr: invoke("releaseLock", [], "ReadableStream async iterator releaseLock"), + loc, + }, + ], + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: DYN }], + locals: frame.locals, + body, + generator: { yieldT: elemT, nextT: UNDEFINED_T, resultType: genResultT }, + loc, + }); + const makerSrc: IrLocal = { id: "src.0", name: "src", type: DYN, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: DYN }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + function lowerIterableSource( lowerer: Lowerer, node: ts.Expression, fn: StageFunction | null, loc: SrcLoc, ): { closure: IrExpr; out: SyncGenType } | null { + if (fn === null) { + const webElem = staticReadableStreamElementType(lowerer, node); + if (webElem !== null) return lowerWebStreamSource(lowerer, node, webElem, loc); + } let value: IrExpr; if (fn !== null) { if (fn.parameters.nodes.length > 0) { @@ -4004,15 +4275,20 @@ function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], lo loc, ); } - for (const a of args) { + // A web ReadableStream first stage reads as an async iterable (the + // pipeline's source); elsewhere it would be Duplex.fromWeb of it. + for (let i = args.length === 1 ? 0 : 1; i < args.length; i++) { + const a = args[i]!; // (typeOf on a stage function's name would report what collecting its // declared signature deferred) if (pipelineStageFunction(lowerer, a) !== null) continue; if (staticReadableStreamElementType(lowerer, a) !== null) { lowerer.noLowering( - "compose with a web ReadableStream", + i === 0 + ? "Duplex.from over a web ReadableStream" + : "compose with a web ReadableStream after the first stage", a, - "web streams do not convert to Node streams yet; compose Node streams, iterables or async generator functions", + "a web ReadableStream is supported as the first stage of compose or pipeline, and in Readable.from", ); } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json new file mode 100644 index 000000000..e17735658 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7428-web-stream-sources.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7428-web-stream-sources.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json new file mode 100644 index 000000000..8707c2f2b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7429-web-stream-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7429-web-stream-sources.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7428-web-stream-sources.mjs b/tests/corpus/7428-web-stream-sources.mjs new file mode 100644 index 000000000..72d98e5f8 --- /dev/null +++ b/tests/corpus/7428-web-stream-sources.mjs @@ -0,0 +1,41 @@ +// A web ReadableStream as an async iterable source: Readable.from reads +// its chunks through the stream's iterator (pull-based and start-based +// sources, Uint8Array chunks in byte mode, an error from the controller, +// and an early destroy that cancels the stream), and it heads compose() +// and pipeline() like any async iterable. +import { Readable, Writable, compose, pipeline } from "node:stream"; +const log = []; +let n = 0; +const pulled = new ReadableStream({ + pull(c) { n++; log.push(`pull ${n}`); if (n <= 3) c.enqueue(`p${n}`); else c.close(); }, + cancel(reason) { log.push(`cancel ${reason}`); }, +}); +for await (const c of Readable.from(pulled)) log.push(`got ${c}`); +const bytes = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hi")); c.enqueue(new TextEncoder().encode("!")); c.close(); } }); +const br = Readable.from(bytes, { objectMode: false }); +br.on("data", (b) => log.push(`bytes ${Buffer.isBuffer(b)} ${b.toString()}`)); +await new Promise((res) => br.on("end", res)); +const failing = new ReadableStream({ start(c) { c.enqueue("ok"); c.error(new Error("web failed")); } }); +const fr = Readable.from(failing); +fr.on("data", (c) => log.push(`failing data ${c}`)); +fr.on("error", (e) => log.push(`failing error ${e.message}`)); +await new Promise((res) => fr.on("close", res)); +let m = 0; +const endless = new ReadableStream({ + pull(c) { c.enqueue(`e${++m}`); }, + cancel(reason) { log.push(`endless cancel ${reason === undefined ? "undefined" : reason}`); }, +}); +const er = Readable.from(endless); +er.once("data", (c) => { log.push(`endless ${c}`); er.destroy(); }); +await new Promise((res) => er.on("close", res)); +await new Promise((res) => setTimeout(res, 5)); +const head = compose(new ReadableStream({ start(c) { c.enqueue("c1"); c.enqueue("c2"); c.close(); } }), async function* (s) { for await (const c of s) yield `${c}!`; }); +log.push(`compose ${(await head.toArray()).join(",")}`); +await new Promise((resolve) => + pipeline( + new ReadableStream({ start(c) { c.enqueue("q1"); c.enqueue("q2"); c.close(); } }), + new Writable({ objectMode: true, write(c, e, cb) { log.push(`pipeline ${c}`); cb(); } }), + (err) => { log.push(`pipeline callback ${err}`); resolve(undefined); }, + ), +); +console.log(log.join("\n")); diff --git a/tests/corpus/7429-web-stream-sources.ts b/tests/corpus/7429-web-stream-sources.ts new file mode 100644 index 000000000..8243b6873 --- /dev/null +++ b/tests/corpus/7429-web-stream-sources.ts @@ -0,0 +1,42 @@ +// Web ReadableStreams as sources in typed code: Readable.from over a +// ReadableStream, interleaved with nextTick callbacks, and a +// ReadableStream heading compose() into an async generator. +import { Readable, compose } from "node:stream"; + +const log: string[] = []; + +function words(): ReadableStream { + return new ReadableStream({ + start(controller): void { + controller.enqueue("typed"); + controller.enqueue("web"); + controller.close(); + }, + }); +} + +async function main(): Promise { + let n = 0; + const tick = (): void => { + log.push(`tick ${n}`); + if (++n < 4) process.nextTick(tick); + }; + process.nextTick(tick); + const r = Readable.from(words()); + log.push(`object mode ${r.readableObjectMode}`); + for await (const c of r) log.push(`word ${String(c)}`); + const bytes = new ReadableStream({ + start(controller): void { + controller.enqueue(new TextEncoder().encode("ab")); + controller.enqueue(new TextEncoder().encode("cd")); + controller.close(); + }, + }); + const lengths = compose(bytes, async function* (source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield `${chunk.length}`; + }); + log.push(`lengths ${(await lengths.toArray()).join(",")}`); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json b/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json new file mode 100644 index 000000000..e95faaa64 --- /dev/null +++ b/tests/harness/corpus-costs/7428-web-stream-sources.mjs.json @@ -0,0 +1 @@ +{ "plain": 1829, "san": 2414 } diff --git a/tests/harness/corpus-costs/7429-web-stream-sources.ts.json b/tests/harness/corpus-costs/7429-web-stream-sources.ts.json new file mode 100644 index 000000000..6b14e36cc --- /dev/null +++ b/tests/harness/corpus-costs/7429-web-stream-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 1250, "san": 3013 } From f4365b7eea5dc2d7f0cb2901d0be7a1bd3bb08e7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 15:02:41 -0500 Subject: [PATCH 043/169] Cache each constant method name's id in a slot A checked-dynamic method call looked its name up in the method table on every call: a strlen, a hash and a byte comparison of the name. The compiler now gives each method-name constant an i32 slot that starts at -1 and passes it with the call (scr_dyn_invoke_prepared_id, and the Buffer method entries, which forward it). The first call stores the name's id in the slot, and later calls read it. Ids stay the runtime's own, so the slot holds whatever this runtime's table says. Names are still matched by content on the first call, and the slot is written with a relaxed atomic store, since every thread writes the same id. The runtime ABI is now 28. --- packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/emit-module.ts | 2 + packages/compiler/src/backend/llvm/emitter.ts | 14 +++++++ .../compiler/src/backend/llvm/expr-dynamic.ts | 8 ++-- packages/compiler/src/backend/llvm/js-ic.ts | 2 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_bytes_dyn.c | 42 +++++++++++-------- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 22 ++++++---- packages/runtime/src/scr_dyn_invoke.c | 23 +++++++--- .../self-hosting-native-toolchain.test.ts | 2 +- 17 files changed, 90 insertions(+), 49 deletions(-) diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a7a5e718a..a1b673630 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v27"); + expect(error?.stderr).toContain("scr_runtime_abi_v28"); }); }); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..47d92e3bd 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -727,6 +727,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); } if (emitter.mapOpsConsts.size > 0) out.push(``); + for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`); + if (emitter.methodIdSlots.size > 0) out.push(``); for (const line of ffiCallbacks.globals) out.push(line); if (ffiCallbacks.globals.length > 0) out.push(``); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..ee8e69e94 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -782,6 +782,10 @@ export class LlEmitter { readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ readonly mapOpsConsts = new Map(); + /** Method-id slots of checked-dynamic method calls + * (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the + * runtime fills with the name's id on the first call. */ + readonly methodIdSlots = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ @@ -2400,6 +2404,16 @@ export class LlEmitter { return `@${c.sym}`; } + /** The method-id slot for a constant method name (methodIdSlots). */ + methodIdSlot(name: string): string { + let sym = this.methodIdSlots.get(name); + if (!sym) { + sym = this.contentSymbol("sc_mid_", name); + this.methodIdSlots.set(name, sym); + } + return `@${sym}`; + } + needBadTag(): void { this.needsBadTag = true; } diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 58bb90569..e3d0dc8f2 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -250,8 +250,10 @@ export function emitDynamicExpr( } // Buffer.prototype method names take their scr_bytes_dyn.c entry, which // runs them on Buffer receivers and dispatches any other receiver. - const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared"; - host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); + // Each call passes its method name's id slot, so only the first call + // looks the name up. + const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -260,7 +262,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 45c18b803..7516673cd 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { /** dynInvoke's method read on an ordinary object through the site's * cache, else scr_dyn_prepare_method. Returns the owned method (or NULL, - * which scr_dyn_invoke_prepared answers with the full dispatch). */ + * which scr_dyn_invoke_prepared_id answers with the full dispatch). */ export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string { const B = host.B; const method = e.method; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 3a41d9176..a616dbec2 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 27 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; +export const RUNTIME_ABI_VERSION = 28 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index af61e5229..c2536ede8 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index d09509911..33b1ca38e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 35a747d60..71b9570b4 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v28"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 631750cdf..3d22b2360 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 0e81033ad..909412bc7 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index 280aa423c..c492d43b1 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,8 +509,9 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -518,40 +519,45 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -584,5 +590,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index a3ac43cc0..9bd5abf8c 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v27(void) {} +void scr_runtime_abi_v28(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index fea020d16..35332d0ca 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v27(void); +void scr_runtime_abi_v28(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bb3b06847..2bdf95562 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1272,15 +1272,19 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), - * one entry per method family: a Buffer receiver runs the method, any other - * takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for a call site whose method name is a constant: + * *mid_slot, the site's -1-initialized slot, caches the name's method id + * after the first call, so later calls skip the name lookup. */ +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared_id for Buffer.prototype method names + * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs + * the method, any other takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index b0f30ac74..d4189f070 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -724,7 +724,7 @@ static bool dyn_bytes_proto_real(int m) { } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); /* Live native arrays: the constant-time methods that loops call once per @@ -796,13 +796,26 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - return scr_dyn_invoke_impl(recv, method, args, argc, what, NULL); + return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); +} + +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { + if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + /* The slot is the call site's, shared by every thread: each writes the + * same id. */ + int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); + if (mid < 0) { + mid = dyn_method_of(method); + __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); + } + return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { - const int mid = dyn_method_of(method); if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; scr_jb_init(&b); @@ -874,7 +887,7 @@ static ScrDyn *scr_dyn_invoke_impl( bool mutates = materialized->kind == SCR_DYN_ARR && dyn_arr_proto_mutates(mid); ScrDyn *result = scr_dyn_invoke_impl( - materialized, method, args, argc, what, recv); + materialized, method, mid, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); if (result == materialized) { scr_dyn_release(result); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c0176eb93..c4013d288 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, + runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 5c29845909601d6e8195e36ef048938209bbe358 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 15:23:28 -0500 Subject: [PATCH 044/169] Support Readable.fromWeb over web ReadableStreams --- .../builtin-apis/streams-and-readline.mdx | 3 +- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 10 + .../compiler/src/backend/llvm/lib-shared.ts | 4 +- .../compiler/src/backend/native/executable.ts | 2 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 140 +++++++++- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/runtime-features.ts | 1 + packages/compiler/src/ir/validate.ts | 16 ++ .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7430-readable-from-web.mjs.json | 6 + .../corpus/7431-readable-from-web.ts.json | 6 + .../corpus/7432-readable-push-views.mjs.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_fetch.c | 48 +++- packages/runtime/src/scr_generator.h | 12 + packages/runtime/src/scr_stream.c | 19 +- packages/runtime/src/scr_stream.h | 7 + packages/runtime/src/scr_stream_from_web.c | 247 ++++++++++++++++++ tests/corpus/7430-readable-from-web.mjs | 42 +++ tests/corpus/7431-readable-from-web.ts | 47 ++++ tests/corpus/7432-readable-push-views.mjs | 16 ++ .../7430-readable-from-web.mjs.json | 1 + .../7431-readable-from-web.ts.json | 1 + .../7432-readable-push-views.mjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 34 files changed, 631 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json create mode 100644 packages/runtime/src/scr_stream_from_web.c create mode 100644 tests/corpus/7430-readable-from-web.mjs create mode 100644 tests/corpus/7431-readable-from-web.ts create mode 100644 tests/corpus/7432-readable-push-views.mjs create mode 100644 tests/harness/corpus-costs/7430-readable-from-web.mjs.json create mode 100644 tests/harness/corpus-costs/7431-readable-from-web.ts.json create mode 100644 tests/harness/corpus-costs/7432-readable-push-views.mjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index eab139773..aa0afd352 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -25,7 +25,8 @@ It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`. A web stream and a body function that declares the options argument are unsupported in `Duplex.from`. `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. - A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`; a web stream in a later compose stage, and `Readable.fromWeb` and `Readable.toWeb`, are unsupported. + A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`. + A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported. The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 1f2fb241a..5b917c63b 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, + runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v30"); + expect(error?.stderr).toContain("scr_runtime_abi_v31"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 380f5d814..bd8f78105 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -7290,6 +7290,16 @@ declare module "stream" { readonly destroyed: boolean; readonly errored: Error | null; static from(iterable: unknown, options?: ReadableOptions): Readable; + /* A Readable over a web ReadableStream's reader. */ + static fromWeb( + readableStream: globalThis.ReadableStream, + options?: { + encoding?: string; + highWaterMark?: number; + objectMode?: boolean; + signal?: AbortSignal; + }, + ): Readable; /* The Readable operators: chunks are strings or Buffers, map and * flatMap callbacks return strings or Buffers (flatMap: arrays of * them, or strings), and callbacks that declare the options argument diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index a790ad197..8e433832a 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1121,7 +1121,7 @@ export const LIB_FN_SYMS: Record = { // chaining forms answer the receiver +1. The throwing members // (listeners and option callbacks run synchronously inside) ride the // generic pending check; loop liveness rides USES_TIMERS_LIB_FNS. - "readable.push": "scr_stream_push", + "readable.push": "scr_stream_push_view", "readable.pushStr": "scr_stream_push_str", "readable.pushStrEnc": "scr_stream_push_str_enc", "readable.pushEncoding": "scr_stream_set_push_encoding", @@ -1134,6 +1134,7 @@ export const LIB_FN_SYMS: Record = { "stream.duplexifyPair": "scr_stream_duplexify_pair", "stream.duplexFromPromise": "scr_stream_duplex_from_promise", "stream.duplexFromAsyncFn": "scr_stream_duplex_from_async_fn", + "fetch.readableFromWeb": "scr_stream_readable_from_web", "writable.writeEncoding": "scr_stream_write_encoding", "writable.setDefaultEncoding": "scr_stream_set_default_encoding", "stream.chunkDone": "scr_stream_chunk_done", @@ -1747,6 +1748,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.duplexifyPair", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "fetch.readableFromWeb", "stream.setWritev", "stream.setWritevDefault", "process.activeResources", diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index ad318e623..1b90bb1f1 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -337,6 +337,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])), // Fetch uses the native network, TLS, and HTTP runtime. unit((u) => u.nativeFetch, "scr_fetch.c"), + // Readable.fromWeb reads a web ReadableStream through its reader. + unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"), ]; /** The arguments of the optional runtime inputs, in link order. */ diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index fcc8d160e..462f15a9e 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 30 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v30" as const; +export const RUNTIME_ABI_VERSION = 31 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v31" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 58e2b4855..4292b93b2 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, + runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index b898a3e62..a5cb5dabe 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, + runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index a7f1469a9..7d15e82bb 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1814,6 +1814,7 @@ export function lowerStreamStaticCall( } } } + if (member === "fromWeb" && cls === "%Readable") return lowerReadableFromWeb(lowerer, call, loc); if (member !== "from") { lowerer.noLowering(`${cls.slice(1)}.${member}`, call); } @@ -1846,6 +1847,112 @@ export function lowerStreamStaticCall( return lowerFromSource(lowerer, call, args[0], args[1], false); } +/** Readable.fromWeb(readableStream, options): Node's adapter over the web + * stream's reader (scr_stream_from_web.c). The options are an object + * literal: objectMode a boolean literal, highWaterMark a number, encoding + * a literal name, and signal a variable or property read, applied with + * addAbortSignal as the constructors do. */ +function lowerReadableFromWeb(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { + const args = call.arguments.nodes; + const readable: IrType = { kind: "object", className: "%Readable" }; + if (args.length === 0 || args.length > 2 || args.some((a) => ts.isSpreadElement(a))) { + lowerer.noLowering( + `Readable.fromWeb with ${args.length} arguments`, + call, + "the supported form is Readable.fromWeb(readableStream, options)", + ); + } + const web = lowerer.lowerExprExpecting(args[0]!, DYN); + let objectMode = false; + let hwm: IrExpr = numLit(-1, loc); + let encoding = ""; + let signal: IrExpr | null = null; + const options = args[1]; + if (options !== undefined) { + if (!ts.isObjectLiteralExpression(options)) { + lowerer.noLowering( + "Readable.fromWeb options that are not an object literal", + options, + "pass { objectMode, highWaterMark, encoding, signal } as an object literal", + ); + } + for (const p of options.properties.nodes) { + const key = + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) + ? p.name.text + : null; + const value = + key === null + ? null + : ts.isShorthandPropertyAssignment(p) + ? (p.name as ts.Identifier) + : (p as ts.PropertyAssignment).initializer; + if (key === "objectMode" && value) { + if (value.kind === ts.SyntaxKind.TrueKeyword) objectMode = true; + else if (value.kind !== ts.SyntaxKind.FalseKeyword) + lowerer.noLowering( + "the Readable.fromWeb option 'objectMode' with this value", + p, + "write true or false", + ); + continue; + } + if (key === "highWaterMark" && value) { + hwm = lowerer.lowerExprExpecting(value, F64); + continue; + } + if (key === "encoding" && value) { + const t = lowerer.typeOf(value); + if (t.isStringLiteralType() && knownBufEncoding(t.value) === undefined) { + return nodeThrowExpr( + 1, + "ERR_INVALID_ARG_VALUE", + `The property 'options.encoding' is invalid. Received '${t.value}'`, + readable, + loc, + ); + } + encoding = bufEncoding(lowerer, "the Readable.fromWeb option 'encoding'", value); + continue; + } + if (key === "signal" && value) { + if (!sideEffectFreeOptionValue(value) && !pureRead(value)) { + lowerer.noLowering( + "the Readable.fromWeb option 'signal' with this value", + p, + "pass a variable or property read (const signal = controller.signal)", + ); + } + signal = lowerer.lowerExprExpecting(value, DYN); + continue; + } + lowerer.noLowering( + key === null + ? "Readable.fromWeb options with this member" + : `the Readable.fromWeb option '${key}'`, + p, + "objectMode, highWaterMark, encoding and signal are the Readable.fromWeb options", + ); + } + } + const built: IrExpr = { + kind: "libCall", + fn: "fetch.readableFromWeb", + args: [web, boolLit(objectMode, loc), hwm, strLit(encoding, loc)], + type: readable, + loc, + }; + if (signal === null) return built; + return { + kind: "libCall", + fn: "stream.addAbortSignal", + args: [built, signal], + type: readable, + loc, + }; +} + /** Readable.from(source, options), and for Duplex.from over an iterable * (duplex) duplexify.js's from(Duplexify, body, { objectMode: true, * writable: false }): the same builders with SCR_FROM_DUPLEX, answering a @@ -1865,16 +1972,24 @@ function lowerFromSource( const duplexFlag = duplex ? 32 : 0; const js = isJsSourceFile(ts.getSourceFileOfNode(call)); // A web ReadableStream is async iterable: from.js reads its iterator. - // (Duplex.from takes Readable.fromWeb of it instead.) + // Duplex.from takes _duplexify({ readable: Readable.fromWeb(body) }). const webElem = srcNode ? staticReadableStreamElementType(lowerer, srcNode) : null; + if (srcNode && webElem !== null && duplex) { + const fromWeb: IrExpr = { + kind: "libCall", + fn: "fetch.readableFromWeb", + args: [ + lowerer.lowerExprExpecting(srcNode, DYN), + boolLit(false, loc), + numLit(-1, loc), + strLit("", loc), + ], + type: { kind: "object", className: "%Readable" }, + loc, + }; + return { kind: "libCall", fn: "stream.duplexifyReadable", args: [fromWeb], type, loc }; + } if (srcNode && webElem !== null) { - if (duplex) { - lowerer.noLowering( - "Duplex.from over a web ReadableStream", - srcNode, - "Readable.from(stream) reads a web ReadableStream", - ); - } const source = lowerWebStreamSource(lowerer, srcNode, webElem, loc); const helpers = source ? fromGenHelpers(lowerer, source.out, loc) : null; const opts = optsNode ? fromOptions(lowerer, optsNode) : { flags: 0, hwm: null }; @@ -4276,17 +4391,16 @@ function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], lo ); } // A web ReadableStream first stage reads as an async iterable (the - // pipeline's source); elsewhere it would be Duplex.fromWeb of it. - for (let i = args.length === 1 ? 0 : 1; i < args.length; i++) { + // pipeline's source), and alone it is Duplex.from of it; a later stage + // would be Duplex.fromWeb of it. + for (let i = 1; i < args.length; i++) { const a = args[i]!; // (typeOf on a stage function's name would report what collecting its // declared signature deferred) if (pipelineStageFunction(lowerer, a) !== null) continue; if (staticReadableStreamElementType(lowerer, a) !== null) { lowerer.noLowering( - i === 0 - ? "Duplex.from over a web ReadableStream" - : "compose with a web ReadableStream after the first stage", + "compose with a web ReadableStream after the first stage", a, "a web ReadableStream is supported as the first stage of compose or pipeline, and in Readable.from", ); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 734f9ee80..af2f2a7cc 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -386,6 +386,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "stream.duplexFromDyn", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "fetch.readableFromWeb", "stream.destroy", "stream.destroyErr", "stream.iteratorClose", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index ad8b907da..9b23a40da 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -170,6 +170,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.streamFrom": { argTypes: [null], result: DYN }, // The chunk/result record depends on ReadableStream; validated by validate.ts. "fetch.readerRead": { argTypes: [DYN], result: VOID }, + /** Readable.fromWeb (scr_stream_from_web.c): the stream, objectMode, + * highWaterMark (< 0: the mode's default) and encoding ("": none). */ + "fetch.readableFromWeb": { argTypes: [DYN, BOOL, F64, STRING], result: VOID }, "json.parse": { argTypes: [STRING], result: DYN }, "json.parseReviver": { argTypes: [STRING, DYN], result: DYN }, "json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 84b767fab..12baeea57 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -223,6 +223,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.duplexFromDyn", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "fetch.readableFromWeb", "stream.duplexifyReadable", "stream.duplexifyWritable", "stream.duplexifyPair", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index d08d0084f..8c9a0432e 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4288,6 +4288,22 @@ function validateFunction( } return true; } + if (e.fn === "fetch.readableFromWeb") { + // Readable.fromWeb: (stream, objectMode, highWaterMark, encoding) → a Readable. + if ( + e.args.length !== 4 || + e.args[0]?.type.kind !== "dyn" || + e.args[1]?.type.kind !== "bool" || + e.args[2]?.type.kind !== "f64" || + e.args[3]?.type.kind !== "string" || + e.type.kind !== "object" + ) + err( + `libCall ${e.fn} must take (stream, objectMode, highWaterMark, encoding) and return a Readable`, + e.loc, + ); + return true; + } if (e.fn === "fetch.readerRead") { if (e.type.kind !== "promise" || e.type.inner.kind !== "record") { err(`libCall fetch.readerRead must return a promise of a read-result record`, e.loc); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 169e89c2e..7f819bdad 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v30"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v31"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v30"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v31"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json new file mode 100644 index 000000000..3e26c3ba0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7430-readable-from-web.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7430-readable-from-web.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json new file mode 100644 index 000000000..6461c53af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7431-readable-from-web.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7431-readable-from-web.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json new file mode 100644 index 000000000..ffde7918d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7432-readable-push-views.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7432-readable-push-views.mjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index aa251dc34..e04a0dfcf 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, + runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 1c0458d7e..d0194be05 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -161,6 +161,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], + ["scr_stream_from_web.c", all("stream", "nativeFetch")], ]; export function variantsFor(source) { diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index acf463824..dd34cf10b 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, + runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 5b7f8da0f..194a14b26 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v30(void) {} +void scr_runtime_abi_v31(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 57928cfb5..9f27a15b1 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v30(void); +void scr_runtime_abi_v31(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 735183b89..96e90e47c 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -44,7 +44,9 @@ typedef struct SfChunk { } SfChunk; struct SfReadRequest { - ScrPromise *promise; + ScrPromise *promise; /* or NULL for a sink request */ + const ScrFetchReadSink *sink; /* readableStreamDefaultReaderRead's read request */ + void *sink_ctx; SfReadRequest *next; }; @@ -776,6 +778,11 @@ static SfReadRequest *sf_reader_take_request(SfReader *r) { static void sf_reader_reject_message_all(SfReader *r, const char *message) { SfReadRequest *request; while ((request = sf_reader_take_request(r)) != NULL) { + if (request->sink) { + request->sink->error(request->sink_ctx, NULL); + free(request); + continue; + } sf_type_error(message); scr_promise_reject_pending(request->promise); scr_promise_release(request->promise); @@ -958,6 +965,12 @@ static void sf_reader_fulfill_one(SfReader *r, bool done, ScrDyn *value) { if (!r) return; SfReadRequest *request = sf_reader_take_request(r); if (!request) return; + if (request->sink) { + if (done) request->sink->close(request->sink_ctx); + else request->sink->chunk(request->sink_ctx, value); + free(request); + return; + } scr_promise_fulfill_ref(request->promise, sf_read_result(done, value), &scr_dyn_retain_v, &scr_dyn_release_v, NULL); scr_promise_release(request->promise); @@ -968,6 +981,11 @@ static void sf_reader_reject_all(SfReader *r, ScrDyn *reason) { if (!r) return; SfReadRequest *request; while ((request = sf_reader_take_request(r)) != NULL) { + if (request->sink) { + request->sink->error(request->sink_ctx, reason); + free(request); + continue; + } sf_reject_promise_reason(request->promise, reason); scr_promise_release(request->promise); free(request); @@ -1654,18 +1672,21 @@ static SfReader *sf_stream_get_reader(SfStream *s) { return r; } -static ScrPromise *sf_reader_read(SfReader *r) { - ScrPromise *p = scr_promise_new(); +/* readableStreamDefaultReaderRead: queue the request, then serve it from + * the queue or a pull. false (TypeError pending) for a released reader. */ +static bool sf_reader_read_request(SfReader *r, ScrPromise *p, const ScrFetchReadSink *sink, + void *sink_ctx) { SfStream *s = r->stream; if (!s || s->reader != r) { sf_type_error("This reader has been released"); - scr_promise_reject_pending(p); - return p; + return false; } s->disturbed = true; SfReadRequest *request = calloc(1, sizeof *request); if (!request) sf_oom(); - request->promise = scr_promise_retain(p); + request->promise = p ? scr_promise_retain(p) : NULL; + request->sink = sink; + request->sink_ctx = sink_ctx; if (r->pending_tail) r->pending_tail->next = request; else r->pending_head = request; r->pending_tail = request; @@ -1674,6 +1695,12 @@ static ScrPromise *sf_reader_read(SfReader *r) { sf_stream_pull(s); sf_stream_drain(s); } + return true; +} + +static ScrPromise *sf_reader_read(SfReader *r) { + ScrPromise *p = scr_promise_new(); + if (!sf_reader_read_request(r, p, NULL, NULL)) scr_promise_reject_pending(p); return p; } @@ -1960,6 +1987,15 @@ ScrDyn *scr_fetch_stream_from_string(ScrStr *iterable) { return out; } +bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx) { + if (!reader || reader->kind != SCR_DYN_HANDLE || + reader->v.handle.tag != SCR_DYNH_WEB_READER) { + sf_type_error("Expected a ReadableStreamDefaultReader"); + return false; + } + return sf_reader_read_request((SfReader *)reader->v.handle.ptr, NULL, sink, ctx); +} + ScrPromise *scr_fetch_reader_read(ScrDyn *reader) { if (!reader || reader->kind != SCR_DYN_HANDLE || reader->v.handle.tag != SCR_DYNH_WEB_READER) { diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c4397e021..c3dac861d 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -200,5 +200,17 @@ ScrDyn *scr_fetch_stream_from_bytes( ScrDyn *scr_fetch_stream_from_string( ScrStr *iterable); /* borrowed string; +1 handle */ ScrPromise *scr_fetch_reader_read(ScrDyn *reader); /* borrowed handle; +1 */ +/* A read request served by callbacks instead of a promise (the streams + * spec's readableStreamDefaultReaderRead with a read request; Node's + * Readable.fromWeb adapter uses one). chunk and error borrow their value; + * error's reason is NULL when the reader was released. */ +typedef struct ScrFetchReadSink { + void (*chunk)(void *ctx, ScrDyn *value); + void (*close)(void *ctx); + void (*error)(void *ctx, ScrDyn *reason); +} ScrFetchReadSink; +/* Queue a sink read request on a reader handle (borrowed). false with a + * TypeError pending for a value that is not a reader or a released one. */ +bool scr_fetch_reader_read_into(ScrDyn *reader, const ScrFetchReadSink *sink, void *ctx); #endif /* SCR_GENERATOR_H */ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 992203eb4..c4910af6a 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -1129,7 +1129,7 @@ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); - bool r = scr_stream_push(s, b); + bool r = scr_stream_push_view(s, b); scr_bytes_release(b); return r; } @@ -6348,6 +6348,23 @@ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { return scr_stream_add_chunk(s, chunk, false); } +/* readable.push(chunk) from a program: in byte mode another + * ArrayBufferView becomes a Buffer over the same bytes + * (Stream._uint8ArrayToBuffer); the runtime's own producers push Buffers + * through scr_stream_push. */ +bool scr_stream_push_view(ScrStream *s, ScrBytes *chunk) { + if (chunk && !chunk->is_buffer && !(s->st->has_r && s->st->r.object_entries)) { + ScrBytes *b = scr_bytes_buffer_view(chunk, SCR_BYTES_U8, scr_bytes_byte_offset(chunk), true, + scr_bytes_byte_len(chunk)); + if (!b) return false; + b->is_buffer = true; + bool r = scr_stream_add_chunk(s, b, false); + scr_bytes_release(b); + return r; + } + return scr_stream_add_chunk(s, chunk, false); +} + bool scr_stream_push_str(ScrStream *s, ScrStr *str) { /* object mode: the string is one whole entry, not converted */ if (s->st->has_r && s->st->r.object_entries) diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index d276700c8..cdc51466e 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -266,6 +266,9 @@ ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc) /* The readable surface. push/unshift borrow their chunk (strings convert * utf8, Node's decodeStrings default); push answers the below-hwm bool. */ bool scr_stream_push(ScrStream *s, ScrBytes *chunk); +/* readable.push(chunk) from a program: a non-Buffer view becomes a Buffer + * over the same bytes in byte mode. */ +bool scr_stream_push_view(ScrStream *s, ScrBytes *chunk); bool scr_stream_push_str(ScrStream *s, ScrStr *str); /* push(chunk, enc): the per-call literal encoding (canonical); overrides * the stream's defaultEncoding. Borrows both. */ @@ -545,6 +548,10 @@ void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritev ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, ScrClosure *read, ScrClosure *ret, ScrClosure *thr); ScrStream *scr_stream_duplex_from_dyn(struct ScrDyn *body); +/* Readable.fromWeb(readableStream, { objectMode, highWaterMark, encoding }) + * (scr_stream_from_web.c): hwm < 0 is the mode's default, encoding NULL or + * a known name; web and encoding borrowed. +1, or NULL with the error. */ +ScrStream *scr_stream_readable_from_web(struct ScrDyn *web, bool object_mode, double hwm, ScrStr *encoding); /* Duplex.from over an async function (the compiled stage takes the source * generator and answers its promise; source and the promise's box helper * borrowed): a writable Duplexify whose final waits for the promise. +1. */ diff --git a/packages/runtime/src/scr_stream_from_web.c b/packages/runtime/src/scr_stream_from_web.c new file mode 100644 index 000000000..f88826335 --- /dev/null +++ b/packages/runtime/src/scr_stream_from_web.c @@ -0,0 +1,247 @@ +/* Readable.fromWeb (Node's newStreamReadableFromReadableStream in + * internal/webstreams/adapters.js): a Readable over a web ReadableStream's + * reader. read() queues one read request (scr_fetch_reader_read_into); a + * chunk is pushed in a microtask, the end pushes null and an error + * destroys the Readable. destroy() cancels the reader unless it has + * closed, then completes. reader.closed's settlement marks it closed (a + * rejection also destroys the Readable). Linked only by programs that + * use both streams and the native web streams. */ +#include "scr_runtime.h" +#include "scr_stream.h" + +#include +#include + +typedef struct SfwCtx SfwCtx; +struct SfwCtx { + size_t rc; + ScrStream *readable; /* +1 until teardown */ + ScrDyn *reader; /* +1 */ + ScrDyn *pending; /* +1: pendingChunk, or NULL */ + bool closed; + SfwCtx *live_prev, *live_next; +}; + +static SCR_TL SfwCtx *sfw_live = NULL; + +static SfwCtx *sfw_ref(SfwCtx *c) { + c->rc++; + return c; +} + +static void sfw_unref(SfwCtx *c) { + if (--c->rc > 0) return; + if (c->readable) scr_stream_release(c->readable); + if (c->reader) scr_dyn_release(c->reader); + if (c->pending) scr_dyn_release(c->pending); + free(c); +} + +static void *sfw_retain_v(void *p) { return sfw_ref(p); } +static void sfw_release_v(void *p) { sfw_unref(p); } + +/* Drop the Readable (the cycle through its callbacks). */ +static void sfw_teardown(SfwCtx *c) { + if (c->live_prev) c->live_prev->live_next = c->live_next; + else if (sfw_live == c) sfw_live = c->live_next; + else return; /* already torn down */ + if (c->live_next) c->live_next->live_prev = c->live_prev; + c->live_prev = c->live_next = NULL; + ScrStream *r = c->readable; + c->readable = NULL; + if (r) scr_stream_release(r); + sfw_unref(c); /* the live list's reference */ +} + +static void sfw_atexit(void) { + while (sfw_live) sfw_teardown(sfw_live); +} + +static ScrClosure *sfw_closure(void *fn, SfwCtx *c) { + ScrClosure *cl = scr_closure_new(fn, 1); + cl->caps[0] = scr_box_new_obj(&sfw_retain_v, &sfw_release_v, NULL); + scr_box_set_ref(cl->caps[0], sfw_ref(c)); + return cl; +} + +static SfwCtx *sfw_of(ScrClosure *cl) { return scr_box_get_ref(cl->caps[0]); /* +1 */ } + +/* destroyer(readable, error) */ +static void sfw_destroyer(SfwCtx *c, ScrDyn *reason) { + ScrStream *r = c->readable; + if (!r || scr_stream_prop(r, "destroyed")) return; + ScrError *err = reason ? scr_error_from_dyn(reason) : NULL; + if (scr_exc_pending()) scr_exc_clear(); + scr_stream_release(scr_stream_destroy(r, err)); + if (err) scr_error_release(err); +} + +/* forwardChunk: readable.push(pendingChunk) */ +static void sfw_forward(ScrClosure *job) { + SfwCtx *c = sfw_of(job); + ScrDyn *chunk = c->pending; + c->pending = NULL; + if (chunk && c->readable) { + scr_stream_push_dyn(c->readable, chunk); + if (scr_exc_pending()) scr_exc_clear(); + } + if (chunk) scr_dyn_release(chunk); + sfw_unref(c); +} + +static void sfw_sink_chunk(void *ctx, ScrDyn *value) { + SfwCtx *c = ctx; + if (c->pending) scr_dyn_release(c->pending); + c->pending = scr_dyn_retain(value); + scr_queue_microtask(sfw_closure((void *)&sfw_forward, c)); + sfw_unref(c); /* the request's reference */ +} + +static void sfw_sink_close(void *ctx) { + SfwCtx *c = ctx; + if (c->readable) scr_stream_push_null(c->readable); + if (scr_exc_pending()) scr_exc_clear(); + sfw_unref(c); +} + +static void sfw_sink_error(void *ctx, ScrDyn *reason) { + SfwCtx *c = ctx; + sfw_destroyer(c, reason); + sfw_unref(c); +} + +static const ScrFetchReadSink sfw_sink = {&sfw_sink_chunk, &sfw_sink_close, &sfw_sink_error}; + +/* read(): readableStreamDefaultReaderRead(reader, readRequest) */ +static void sfw_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)s; + (void)size; + SfwCtx *c = sfw_of(cb); + if (c->reader) { + if (!scr_fetch_reader_read_into(c->reader, &sfw_sink, sfw_ref(c))) { + sfw_unref(c); + if (scr_exc_pending()) scr_exc_clear(); + } + } + sfw_unref(c); +} + +/* destroy's done(): callback(error) */ +typedef struct { + SfwCtx *c; /* +1 */ + ScrStream *s; /* +1 */ + ScrError *err; /* +1 or NULL */ + ScrDyn *cancel; /* +1: reader.cancel(error)'s promise */ +} SfwDestroy; + +static void sfw_destroy_done(SfwCtx *c, ScrStream *s, ScrError *err) { + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); + sfw_teardown(c); +} + +static void sfw_cancel_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + SfwDestroy d = *(SfwDestroy *)arg; + free(arg); + ScrDyn *v = scr_await_ref(d.cancel->v.promise); /* then(done, done) */ + if (scr_exc_pending()) scr_exc_clear(); + if (v) scr_dyn_release(v); + sfw_destroy_done(d.c, d.s, d.err); + if (d.err) scr_error_release(d.err); + scr_stream_release(d.s); + scr_dyn_release(d.cancel); + sfw_unref(d.c); +} + +static void sfw_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + SfwCtx *c = sfw_of(cb); + if (!c->closed && c->reader) { + /* _destroy(err, cb) gets null for a destroy without an error */ + ScrDyn *reason = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + ScrDyn *cancel = scr_dyn_invoke(c->reader, "cancel", &reason, 1, "reader.cancel"); + scr_dyn_release(reason); + if (cancel && cancel->kind == SCR_DYN_PROMISE) { + SfwDestroy *d = malloc(sizeof *d); + if (!d) abort(); + d->c = sfw_ref(c); + d->s = scr_stream_retain(s); + d->err = err ? scr_error_retain(err) : NULL; + d->cancel = cancel; + scr_promise_release(scr_async_spawn_after(cancel->v.promise, &sfw_cancel_entry, d)); + sfw_unref(c); + return; + } + if (cancel) scr_dyn_release(cancel); + if (scr_exc_pending()) scr_exc_clear(); + } + sfw_destroy_done(c, s, err); + sfw_unref(c); +} + +/* reader.closed.then(() => closed = true, (e) => { closed = true; destroy(readable, e) }) */ +typedef struct { + SfwCtx *c; /* +1 */ + ScrDyn *closed; /* +1, the promise */ +} SfwClosed; + +static void sfw_closed_entry(ScrFiber *fiber, void *arg) { + (void)fiber; + SfwClosed w = *(SfwClosed *)arg; + free(arg); + ScrDyn *v = scr_await_ref(w.closed->v.promise); + w.c->closed = true; + if (scr_exc_pending()) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *reason = scr_caught_to_dyn(caught); + scr_caught_release(caught); + sfw_destroyer(w.c, reason); + if (reason) scr_dyn_release(reason); + } + if (v) scr_dyn_release(v); + scr_dyn_release(w.closed); + sfw_unref(w.c); +} + +/* web: the ReadableStream (borrowed); hwm < 0 = the mode's default; + * encoding NULL or a known name (borrowed). +1, or NULL with the error + * pending. */ +ScrStream *scr_stream_readable_from_web(ScrDyn *web, bool object_mode, double hwm, ScrStr *encoding) { + if (encoding && encoding->len == 0) encoding = NULL; + if (!web || web->kind != SCR_DYN_HANDLE || web->v.handle.tag != SCR_DYNH_WEB_STREAM) { + scr_dyn_arg_type_fail("readableStream", "an instance of ReadableStream", web); + return NULL; + } + ScrDyn *reader = scr_dyn_invoke(web, "getReader", NULL, 0, "readableStream.getReader"); + if (!reader) return NULL; + SfwCtx *c = calloc(1, sizeof *c); + if (!c) abort(); + c->rc = 1; + c->reader = reader; + ScrStream *r = scr_stream_new_readable(hwm < 0 ? (object_mode ? 16 : -1) : hwm, true, true, + sfw_closure((void *)&sfw_read_inv, c), &sfw_read_inv, + sfw_closure((void *)&sfw_destroy_inv, c), &sfw_destroy_inv); + if (object_mode) scr_stream_set_object_mode(r, true, false); + if (encoding) scr_stream_release(scr_stream_set_encoding(r, encoding)); + c->readable = scr_stream_retain(r); + static SCR_TL bool registered = false; + if (!registered) { + registered = true; + scr_atexit(sfw_atexit); + } + c->live_next = sfw_live; + if (sfw_live) sfw_live->live_prev = c; + sfw_live = c; /* the reference c was created with */ + ScrStr *key = scr_str_new("closed", 6); + ScrDyn *closed = scr_dyn_handle_key_get(reader, key); + scr_str_release(key); + if (closed && closed->kind == SCR_DYN_PROMISE) { + SfwClosed *w = malloc(sizeof *w); + if (!w) abort(); + w->c = sfw_ref(c); + w->closed = closed; + scr_promise_release(scr_async_spawn_after(closed->v.promise, &sfw_closed_entry, w)); + } else if (closed) { + scr_dyn_release(closed); + } + return r; +} diff --git a/tests/corpus/7430-readable-from-web.mjs b/tests/corpus/7430-readable-from-web.mjs new file mode 100644 index 000000000..017556f9d --- /dev/null +++ b/tests/corpus/7430-readable-from-web.mjs @@ -0,0 +1,42 @@ +// Readable.fromWeb: a Readable over a web ReadableStream's reader, in +// byte mode by default (Uint8Array chunks arrive as Buffers), in object +// mode, and with an encoding; an early destroy cancels the stream with +// null, and an error from the stream destroys the Readable. Duplex.from +// and compose() over a lone web ReadableStream read it the same way. +import { Duplex, Readable, compose } from "node:stream"; +const log = []; +const tick = (n) => { if (n < 6) { log.push(`tick ${n}`); process.nextTick(() => tick(n + 1)); } }; +const web = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("ab")); c.enqueue(new TextEncoder().encode("cd")); c.close(); } }); +const r = Readable.fromWeb(web); +log.push(`r ${r.readableObjectMode} ${r.readableHighWaterMark}`); +r.on("data", (b) => log.push(`data ${Buffer.isBuffer(b)} ${b.toString()}`)); +r.on("end", () => log.push("end")); +r.on("close", () => log.push("close")); +tick(0); +await new Promise((res) => r.on("close", res)); +const words = new ReadableStream({ start(c) { c.enqueue("x"); c.enqueue("y"); c.close(); } }); +const o = Readable.fromWeb(words, { objectMode: true, highWaterMark: 1 }); +for await (const v of o) log.push(`obj ${v}`); +const text = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("héllo")); c.close(); } }); +const t = Readable.fromWeb(text, { encoding: "utf8" }); +for await (const v of t) log.push(`text ${typeof v} ${v}`); +let m = 0; +const endless = new ReadableStream({ pull(c) { c.enqueue(new Uint8Array([++m])); }, cancel(reason) { log.push(`cancel ${reason && reason.name}`); } }); +const e = Readable.fromWeb(endless); +e.once("data", (b) => { log.push(`endless ${b[0]}`); e.destroy(); }); +e.on("close", () => log.push("endless close")); +await new Promise((res) => e.on("close", res)); +await new Promise((res) => setTimeout(res, 5)); +const failing = new ReadableStream({ start(c) { c.error(new Error("web broke")); } }); +const f = Readable.fromWeb(failing); +f.on("error", (err) => log.push(`failing error ${err.message}`)); +f.on("close", () => log.push("failing close")); +f.resume(); +await new Promise((res) => f.on("close", res)); +const d = Duplex.from(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("dx")); c.close(); } })); +log.push(`d ${d.readable} ${d.writable} ${d.readableObjectMode}`); +d.on("data", (b) => log.push(`d data ${b.toString()}`)); +await new Promise((res) => d.on("close", res)); +const one = compose(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("one")); c.close(); } })); +for await (const b of one) log.push(`one ${b.toString()}`); +console.log(log.join("\n")); diff --git a/tests/corpus/7431-readable-from-web.ts b/tests/corpus/7431-readable-from-web.ts new file mode 100644 index 000000000..23e7d2871 --- /dev/null +++ b/tests/corpus/7431-readable-from-web.ts @@ -0,0 +1,47 @@ +// Readable.fromWeb in typed code: a ReadableStream read as +// Buffers, a ReadableStream in object mode with a highWaterMark of +// 1, and an AbortSignal that destroys the Readable and cancels the stream. +import { Readable } from "node:stream"; + +const log: string[] = []; + +async function main(): Promise { + const bytes = new ReadableStream({ + start(controller): void { + controller.enqueue(new TextEncoder().encode("web")); + controller.enqueue(new TextEncoder().encode("bytes")); + controller.close(); + }, + }); + const r = Readable.fromWeb(bytes); + for await (const chunk of r) log.push(`bytes ${Buffer.isBuffer(chunk)} ${String(chunk)}`); + const words = new ReadableStream({ + start(controller): void { + controller.enqueue("alpha"); + controller.enqueue("beta"); + controller.close(); + }, + }); + const o = Readable.fromWeb(words, { objectMode: true, highWaterMark: 1 }); + log.push(`object mode ${o.readableObjectMode} ${o.readableHighWaterMark}`); + for await (const word of o) log.push(`word ${String(word)}`); + const ac = new AbortController(); + const signal = ac.signal; + let n = 0; + const endless = new ReadableStream({ + pull(controller): void { + controller.enqueue(new Uint8Array([++n])); + }, + cancel(reason: unknown): void { + log.push(`cancelled ${(reason as Error).name}`); + }, + }); + const a = Readable.fromWeb(endless, { signal }); + a.on("error", (e: Error) => log.push(`aborted ${e.name}`)); + a.once("data", () => ac.abort()); + await new Promise((resolve) => a.on("close", () => resolve())); + await new Promise((resolve) => setTimeout(resolve, 5)); + for (const line of log) console.log(line); +} + +void main(); diff --git a/tests/corpus/7432-readable-push-views.mjs b/tests/corpus/7432-readable-push-views.mjs new file mode 100644 index 000000000..9aca435b6 --- /dev/null +++ b/tests/corpus/7432-readable-push-views.mjs @@ -0,0 +1,16 @@ +// readable.push() in byte mode turns a Uint8Array or another typed array +// into a Buffer over the same bytes (Stream._uint8ArrayToBuffer); the +// pushed array itself is left as it was. +import { Readable } from "node:stream"; +const u = new Uint8Array([100, 101]); +const r = new Readable({ read() {} }); +r.push(u); r.push(new Uint16Array([0x6766])); r.push(null); +for await (const b of r) console.log(Buffer.isBuffer(b), b.toString(), b.length); +console.log("source still", Buffer.isBuffer(u)); +const r1 = new Readable({ read() {} }); +r1.push(new Uint8Array([97, 98])); +r1.push(null); +for await (const b of r1) console.log(Buffer.isBuffer(b), b.toString()); +const r2 = new Readable({ read() {} }); +r2.on("data", (b) => console.log("data", Buffer.isBuffer(b), b.toString())); +r2.push(new Uint8Array([99])); r2.push(null); diff --git a/tests/harness/corpus-costs/7430-readable-from-web.mjs.json b/tests/harness/corpus-costs/7430-readable-from-web.mjs.json new file mode 100644 index 000000000..2266382b4 --- /dev/null +++ b/tests/harness/corpus-costs/7430-readable-from-web.mjs.json @@ -0,0 +1 @@ +{ "plain": 3342, "san": 4411 } diff --git a/tests/harness/corpus-costs/7431-readable-from-web.ts.json b/tests/harness/corpus-costs/7431-readable-from-web.ts.json new file mode 100644 index 000000000..05fab8e59 --- /dev/null +++ b/tests/harness/corpus-costs/7431-readable-from-web.ts.json @@ -0,0 +1 @@ +{ "plain": 1956, "san": 2929 } diff --git a/tests/harness/corpus-costs/7432-readable-push-views.mjs.json b/tests/harness/corpus-costs/7432-readable-push-views.mjs.json new file mode 100644 index 000000000..8a2f5280e --- /dev/null +++ b/tests/harness/corpus-costs/7432-readable-push-views.mjs.json @@ -0,0 +1 @@ +{ "plain": 1582, "san": 2088 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 81ff5b293..e74d9f60a 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 30, marker: "scr_runtime_abi_v30" }, + runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 81bf6cb0c7f744c621fa3225ff4053416b3607fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:14:28 -0500 Subject: [PATCH 045/169] Check integer-or-NaN indexes without a double round trip An array or typed-array index that is an exact integer or NaN, such as an element read that may miss (`t[i]!`), a `charCodeAt`, or a sum of those, is a valid index when it is ordered and below the length. The index check now tests that with one ordered compare and the integer, taken from the value's 32-bit view or from one conversion, instead of converting the index back to a double and comparing it. Number locals that are declared once with an initializer, are not captured or boxed, and are only written such values carry the same fact, so `const c = t[i]!; table[c]` and `(h * 31 + c) | 0` take the integer paths that the inline read already takes. --- .../src/backend/llvm/dense-array-access.ts | 6 + .../src/backend/llvm/emit-function.ts | 2 + packages/compiler/src/backend/llvm/emitter.ts | 3 + .../compiler/src/backend/llvm/expr-bytes.ts | 28 +++- .../llvm/integer-or-nan-indexes.test.ts | 130 ++++++++++++++++++ .../src/backend/llvm/wrapping-sums.ts | 114 ++++++++++++++- .../4814-integer-or-nan-indexes.ts.json | 6 + tests/corpus/4814-integer-or-nan-indexes.ts | 127 +++++++++++++++++ .../4814-integer-or-nan-indexes.ts.json | 1 + 9 files changed, 410 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json create mode 100644 tests/corpus/4814-integer-or-nan-indexes.ts create mode 100644 tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 0ea014059..5d7994d42 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -30,6 +30,7 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import { compactRefLoadLines, compactRefStoreLines } from "./classes.js"; import { elemAccess } from "./shapes.js"; import { exactInteger, widenInteger } from "./integer-values.js"; +import { integerOrNaNIndex } from "./wrapping-sums.js"; const SCR_ARR_VALUE = 1; @@ -77,12 +78,17 @@ function emitDenseGuard( const B = host.B; const size = host.sizeType; const integer = exactInteger(host, index, indexExpr); + const nanIndex = integer ? null : integerOrNaNIndex(host, index, indexExpr); let wide: string; let integral: string | null = null; if (integer) { // A proven integer needs no double round-trip. Negative signed values // compare above any length or capacity as unsigned 64-bit integers. wide = widenInteger(host, integer); + } else if (nanIndex) { + // An exact integer or NaN is an index exactly when it is ordered. + wide = nanIndex.wide; + integral = nanIndex.ordered; } else { // freeze keeps NaN/out-of-range conversions from producing poison; the // exact round-trip comparison then rejects every value that is not an diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 135ebc706..3acdc9cbf 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -56,6 +56,7 @@ import { f64Lit } from "./common.js"; import { RESULT_ENTRY_PREFIX } from "./record-results.js"; import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; import { runningExtremumUpdates } from "./running-extrema.js"; +import { findIntegerOrNaNLocals } from "./wrapping-sums.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); @@ -233,6 +234,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerRanges = rangeFacts.ranges; emitter.integerLoopRanges = rangeFacts.loopRanges; emitter.int32Elements = slotFacts.elements ?? null; + emitter.integerOrNaNLocals = findIntegerOrNaNLocals(emitter, numericFn); emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); emitter.bytesOwned = new Set(); // Debug builds keep every number local a double for the debugger's diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ed5a8ba46..ec0b7229d 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1158,6 +1158,9 @@ export class LlEmitter { int32Elements: ((arr: IrExpr) => boolean) | null = null; /** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */ wrappingSums = new Set(); + /** Number locals of the current function that hold exact integers or NaN + * (findIntegerOrNaNLocals in wrapping-sums.ts). */ + integerOrNaNLocals: ReadonlyMap = new Map(); byteWindowsEnabled = true; byteWindowEntries: ReturnType = new Map(); /** Typed-array loop versioning (bytes-loop-versions.ts) for the current diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 1419cbef7..c16ae856a 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -7,7 +7,7 @@ import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { emitByteNumber } from "./byte-numbers.js"; -import { integerOrNaNRange } from "./wrapping-sums.js"; +import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js"; import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js"; const BYTES_NUM_KIND: Record = { @@ -119,12 +119,28 @@ export function emitBytesIndex( B.line(`${narrow} = trunc i64 ${wideIndex} to ${host.sizeType}`); return narrow; } - const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); - for (const line of exact.lines) B.line(line); + let ok: string; + let wide: string; + const nanIndex = integerOrNaNIndex(host, value, expr); + if (nanIndex) { + // An exact integer or NaN: in range when ordered and below the length. + const wideLength = host.sizeType === "i64" ? len : B.tmp(); + if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`); + const below = B.tmp(); + ok = B.tmp(); + B.line(`${below} = icmp ult i64 ${nanIndex.wide}, ${wideLength}`); + B.line(`${ok} = and i1 ${nanIndex.ordered}, ${below}`); + wide = nanIndex.wide; + } else { + const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); + for (const line of exact.lines) B.line(line); + ok = exact.ok; + wide = exact.wide; + } const invalid = B.newLabel("bytes.index.invalid"); const valid = B.newLabel("bytes.index.valid"); - B.condBr(exact.ok, valid, invalid); - let idx = exact.wide; + B.condBr(ok, valid, invalid); + let idx = wide; B.startBlock(invalid); if (skipInvalid) B.br(skipInvalid); @@ -138,7 +154,7 @@ export function emitBytesIndex( B.startBlock(valid); if (host.sizeType !== "i64") { idx = B.tmp(); - B.line(`${idx} = trunc i64 ${exact.wide} to ${host.sizeType}`); + B.line(`${idx} = trunc i64 ${wide} to ${host.sizeType}`); } return idx; } diff --git a/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts b/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts new file mode 100644 index 000000000..93a0a73ed --- /dev/null +++ b/packages/compiler/src/backend/llvm/integer-or-nan-indexes.test.ts @@ -0,0 +1,130 @@ +import { expect, test } from "vitest"; +import { F64, arrayOf, bytesOf, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "integer-or-nan-indexes.ts", start: 0, end: 0 }; +const nums = arrayOf(F64); +const u8 = bytesOf("u8"); +const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({ + kind: "varRef", + localId, + type, + loc, +}); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +/** `t[i]!`: the element, or NaN for an index that misses. */ +const byteRead = (index: IrExpr): IrExpr => ({ + kind: "bytesIntrinsic", + method: "get", + receiver: ref("t", u8), + args: [index], + type: F64, + loc, + invalidNaN: true, +}); + +function module(body: IrStmt[]): IrModule { + return { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + unions: [], + functions: [ + { name: "__main", params: [], returnType: F64, locals: [], body: [], loc }, + { + name: "read", + params: [ + { name: "a", localId: "a", type: nums }, + { name: "t", localId: "t", type: u8 }, + { name: "i", localId: "i", type: F64 }, + ], + returnType: F64, + locals: [ + { id: "a", name: "a", type: nums, mutable: false }, + { id: "t", name: "t", type: u8, mutable: false }, + { id: "i", name: "i", type: F64, mutable: false }, + { id: "c", name: "c", type: F64, mutable: true }, + ], + body, + loc, + }, + ], + }; +} + +function body(llvm: string): string { + return /^define internal [^\n]*@sc_bf_read(?:\.i32)?\([^]*?^}/m.exec(llvm)![0]; +} + +const count = (text: string, pattern: RegExp): number => text.match(pattern)?.length ?? 0; + +test("an index bound to a byte read that may miss checks only that it is ordered", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { + kind: "return", + value: { + kind: "bin", + op: "+", + left: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + right: { + kind: "bytesIntrinsic", + method: "get", + receiver: ref("t", u8), + args: [{ kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }], + type: F64, + loc, + invalidNaN: true, + }, + type: F64, + loc, + }, + loc, + }, + ]), + ); + const fn = body(llvm); + // Only the parameter index `i` round-trips through a double. + expect(count(fn, /fcmp oeq double/g)).toBe(1); + expect(count(fn, /fcmp ord double/g)).toBe(2); +}); + +test("a local also written an ordinary number keeps the exact index check", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { kind: "assign", localId: "c", value: ref("i", F64), loc }, + { + kind: "return", + value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + loc, + }, + ]), + ); + const fn = body(llvm); + expect(count(fn, /fcmp oeq double/g)).toBe(2); + expect(fn).not.toContain("fcmp ord double"); +}); + +test("a local whose writes grow from its own value keeps the exact index check", () => { + const llvm = emitLlvmModule( + module([ + { kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc }, + { + kind: "assign", + localId: "c", + value: { kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }, + loc, + }, + { + kind: "return", + value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc }, + loc, + }, + ]), + ); + const fn = body(llvm); + expect(count(fn, /fcmp oeq double/g)).toBe(2); + expect(fn).not.toContain("fcmp ord double"); +}); diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index b00b61cbb..c736fcf72 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -1,4 +1,5 @@ -import type { IrExpr } from "../../ir/ir.js"; +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js"; import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; import type { LlInteger } from "./integer-values.js"; @@ -47,6 +48,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR return ELEMENT_RANGES.i32!; // A UTF-16 code unit, or NaN past either end. if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 }; + if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null; if (e.kind === "bin" && (e.op === "+" || e.op === "-")) { const left = integerOrNaNRange(host, e.left); const right = left && integerOrNaNRange(host, e.right); @@ -55,6 +57,34 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR return null; } +/** An index that is an exact integer or NaN (`integerOrNaNRange`): the integer as an i64, and an + * i1 that is false when the index is NaN. An index check then needs no round trip through a double: + * the integer is the value's ToUint32 view where the range proves it holds the whole value, or one + * conversion, which is exact for a safe integer and frozen, since the conversion of NaN is poison. + * Null for any other index. */ +export function integerOrNaNIndex( + host: LlvmEmitterContext, + value: LlValue, + expr: IrExpr | undefined, +): { wide: string; ordered: string } | null { + const range = expr ? integerOrNaNRange(host, expr) : null; + if (!range) return null; + const B = host.B; + const ordered = B.tmp(); + const wide = B.tmp(); + B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`); + if (value.uint32 !== undefined && range.min >= 0 && range.max <= 4294967295) { + B.line(`${wide} = zext i32 ${value.uint32} to i64`); + } else if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) { + B.line(`${wide} = sext i32 ${value.uint32} to i64`); + } else { + const raw = B.tmp(); + B.line(`${raw} = fptosi double ${value.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + } + return { wide, ordered }; +} + const ELEMENT_RANGES: Partial> = { u8: { min: 0, max: 255 }, u8c: { min: 0, max: 255 }, @@ -131,3 +161,85 @@ export function emitWrappingSum( B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`); return out; } + +/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of + * those integers: locals declared once with an initializer, neither parameters nor captured or + * boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once + * the values it is written depend only on locals that have joined, so `x = x + 1` keeps `x` out: + * its values have no bound. Uses the function's `integerRanges` and `int32Elements`. */ +export function findIntegerOrNaNLocals( + host: LlvmEmitterContext, + fn: IrFunction, +): ReadonlyMap { + const found = new Map(); + if (fn.async || fn.generator) return found; + const excluded = new Set(); + for (const param of fn.params) excluded.add(param.localId); + for (const capture of fn.captures ?? []) excluded.add(capture.localId); + for (const capture of fn.classCaptures ?? []) excluded.add(capture.localId); + const candidates = new Set(); + for (const local of fn.locals) + if (local.type.kind === "f64" && !local.boxed && !local.tdz && !excluded.has(local.id)) + candidates.add(local.id); + if (candidates.size === 0) return found; + const declared = new Set(); + const writes = new Map(); + const drop = (id: string): void => { + candidates.delete(id); + writes.delete(id); + }; + const write = (id: string, value: IrExpr): void => { + if (!candidates.has(id)) return; + const list = writes.get(id); + if (list) list.push(value); + else writes.set(id, [value]); + }; + function walkExpr(e: IrExpr): boolean { + if (e.kind === "assignExpr") write(e.localId, e.value); + else if (e.kind === "incDec") drop(e.localId); + return everyExprChild(e, walkExpr, walkStmt); + } + function walkStmt(s: IrStmt): boolean { + if (s.kind === "assign") write(s.localId, s.value); + else if (s.kind === "varDecl") { + if (s.init === null || declared.has(s.localId)) drop(s.localId); + else { + declared.add(s.localId); + write(s.localId, s.init); + } + } else if (s.kind === "forOf") drop(s.localId); + else if (s.kind === "tryCatch" && s.catchLocalId) drop(s.catchLocalId); + return everyStmtChild(s, walkExpr, walkStmt); + } + for (const s of fn.body) walkStmt(s); + // `integerOrNaNRange` reads the joined locals while the next ones are tested. + const previous = host.integerOrNaNLocals; + host.integerOrNaNLocals = found; + try { + let joined = true; + while (joined) { + joined = false; + for (const [id, values] of writes) { + if (found.has(id) || !declared.has(id)) continue; + let range: IntegerRange | null = null; + for (const value of values) { + const next = integerOrNaNRange(host, value); + if (next === null) { + range = null; + break; + } + range = + range === null + ? next + : { min: Math.min(range.min, next.min), max: Math.max(range.max, next.max) }; + } + if (range === null) continue; + found.set(id, range); + joined = true; + } + } + } finally { + host.integerOrNaNLocals = previous; + } + return found; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json new file mode 100644 index 000000000..0a6abb5a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4814-integer-or-nan-indexes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4814-integer-or-nan-indexes.ts" + ], + "diags": [] +} diff --git a/tests/corpus/4814-integer-or-nan-indexes.ts b/tests/corpus/4814-integer-or-nan-indexes.ts new file mode 100644 index 000000000..1bcfa3e02 --- /dev/null +++ b/tests/corpus/4814-integer-or-nan-indexes.ts @@ -0,0 +1,127 @@ +// Typed-array and array indexes that are exact integers or NaN: element +// reads that may miss, bound to locals or used inline, `charCodeAt` past +// either end, and sums of those. A NaN index reads undefined and a write +// to it is ignored; negative and too-large integers miss the same way. +function show(label: string, value: number | undefined): void { + console.log(label, value); +} + +const bytes = new Uint8Array([65, 67, 71, 84, 0, 255, 3]); +const signed = new Int8Array([-1, 2, -128, 127, 5]); +const wide = new Uint32Array([1, 4294967295, 2, 7]); +const halves = new Int16Array([-300, 4, 6]); +const table = new Int32Array(256); +for (let i = 0; i < 256; i++) table[i] = i * 3 - 100; +const list: number[] = []; +for (let i = 0; i < 300; i++) list.push(i * 0.5); + +function lookups(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + const t = table[c]; + acc = (acc * 7 + (t === undefined ? -1 : t)) % 1000003; + } + return acc; +} + +function listLookups(n: number): number { + let acc = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + const v = list[c]; + acc = acc * 3 + (v === undefined ? -2 : v); + } + return acc; +} + +function signedIndexes(): string { + const out: string[] = []; + for (let i = -1; i <= signed.length; i++) { + const s = signed[i]!; + out.push(String(table[s]), String(list[s + 2]), String(bytes[s - 1])); + } + return out.join(","); +} + +function wideIndexes(): string { + const out: string[] = []; + for (let i = 0; i <= wide.length; i++) { + const w = wide[i]!; + out.push(String(bytes[w]), String(list[w]), String(table[w - 1])); + } + for (let i = 0; i <= halves.length; i++) { + const h = halves[i]!; + out.push(String(table[h + 301]), String(list[h])); + } + return out.join(","); +} + +function codeUnits(s: string): number { + let acc = 0; + for (let i = -2; i < s.length + 2; i++) { + const u = s.charCodeAt(i); + const t = table[u]; + acc = acc * 5 + (t === undefined ? 1 : t); + acc %= 1000003; + } + return acc; +} + +function writes(n: number): string { + const counts = new Uint16Array(8); + const tally: number[] = [0, 0, 0, 0]; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + counts[c & 7]++; + counts[c - 60] = 9; + tally[c - 65] = (tally[c - 65] ?? 0) + 1; + } + return `${Array.from(counts).join(" ")} | ${tally.join(" ")} | ${tally.length}`; +} + +function reassigned(n: number): string { + const out: number[] = []; + for (let i = 0; i < n; i++) { + let c = bytes[i]!; + if (c > 200) c = signed[i - 5]!; + out.push(table[c] ?? -7); + // A fraction makes `f` an ordinary number index. + let f = bytes[i]!; + if (i === 2) f = f / 2; + out.push(list[f] ?? -8); + } + return out.join(" "); +} + +function growing(n: number): number { + let x = bytes[0]!; + let acc = 0; + for (let i = 0; i < n; i++) { + x = x + bytes[i]!; + acc = acc + (table[x] ?? -3); + } + return acc; +} + +function hashes(n: number): string { + let h = 0; + let k = 0; + for (let i = 0; i < n; i++) { + const c = bytes[i]!; + h = (h * 31 + c) | 0; + k = ((k << 2) | (c >> 1)) & 0xffff; + } + return `${h} ${k}`; +} + +show("lookups", lookups(bytes.length)); +show("lookups past the end", lookups(bytes.length + 3)); +show("list lookups", listLookups(bytes.length + 2)); +console.log("signed", signedIndexes()); +console.log("wide", wideIndexes()); +show("code units", codeUnits("ACGTÿĀ")); +console.log("writes", writes(bytes.length + 2)); +console.log("reassigned", reassigned(bytes.length + 1)); +show("growing", growing(bytes.length + 1)); +console.log("hashes", hashes(bytes.length + 2)); diff --git a/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json b/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json new file mode 100644 index 000000000..23f4b5b65 --- /dev/null +++ b/tests/harness/corpus-costs/4814-integer-or-nan-indexes.ts.json @@ -0,0 +1 @@ +{"plain":3473,"san":4706} From 846552b0e28a9adc5fa813c207300c3d370c5d1d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:18:54 -0500 Subject: [PATCH 046/169] Support crypto.getCipherInfo, AES-CCM and the AES key wrap ciphers getCipherInfo(nameOrNid[, options]) runs in a new runtime unit, scr_cipher_info.c, over a table that scripts/gen-cipher-info.mjs generates from Node 24.22's getCipherInfo. The table covers every cipher OpenSSL names, including the ones without a native implementation. Lookups match names case-insensitively and accept OpenSSL's nids. The unit applies cipher.js's argument checks and GetCipherInfo's keyLength and ivLength rules, and returns the info object in Node's key order. CipherInfo maps to a checked-dynamic value, so TypeScript and JavaScript code receive Node's object. Reading the nid Node leaves out for the SIV and SM4 GCM, CCM and XTS ciphers, where the declared type gives number, throws a TypeError. createCipheriv and createDecipheriv support aes-128/192/256-ccm (and the id-aesN-ccm names) over OpenSSL's ccm128 steps. They follow Node's tag and IV lengths, the plaintextLength requirement, the message-length caps and the one-update rule, and report a failed authentication from final(). They also support the RFC 3394 and RFC 5649 key wrap ciphers (aes-N-wrap, aesN-wrap, id-aesN-wrap, the -pad forms and the -inv ciphers) over wrap128.c's routines, with Node's lengths, integrity failures and one-update rule. The new names follow the existing ones in the cipher table, so the lookup cost of the existing ciphers is unchanged. --- .../limitations/builtin-apis/crypto.mdx | 4 +- .../src/generated/node-v24-compatibility.json | 6 +- .../generated/node-v24-internal.json | 16 +- internal/compatibility/static-support.json | 5 +- .../ambient/scriptc-node-fallback.d.ts | 60 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/native/runtime-inputs.ts | 1 + .../src/frontend/lowering/builtins/crypto.ts | 31 +- .../src/frontend/lowering/surfaces.ts | 11 +- packages/compiler/src/frontend/type-mapper.ts | 11 + packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/surface-manifest.json | 4 +- .../corpus/7154-crypto-cipher-info.cjs.json | 6 + .../tests/corpus/7155-cipher-aes-ccm.cjs.json | 6 + .../corpus/7156-cipher-key-wrap.cjs.json | 6 + .../7157-crypto-cipher-info-typed.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_cipher.c | 516 ++++++++++++++- packages/runtime/src/scr_cipher_info.c | 161 +++++ packages/runtime/src/scr_cipher_info.h | 615 ++++++++++++++++++ packages/runtime/src/scr_crypto.h | 3 + scripts/gen-cipher-info.mjs | 125 ++++ tests/corpus/7154-crypto-cipher-info.cjs | 57 ++ tests/corpus/7155-cipher-aes-ccm.cjs | 158 +++++ tests/corpus/7156-cipher-key-wrap.cjs | 103 +++ tests/corpus/7157-crypto-cipher-info-typed.ts | 42 ++ tests/fixtures/node-types/fenced.ts | 2 +- .../__snapshots__/node-types-fenced.txt | 10 +- .../7154-crypto-cipher-info.cjs.json | 1 + .../corpus-costs/7155-cipher-aes-ccm.cjs.json | 1 + .../7156-cipher-key-wrap.cjs.json | 1 + .../7157-crypto-cipher-info-typed.ts.json | 1 + tests/harness/errors.test.ts | 37 +- 34 files changed, 1962 insertions(+), 52 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json create mode 100644 packages/runtime/src/scr_cipher_info.c create mode 100644 packages/runtime/src/scr_cipher_info.h create mode 100644 scripts/gen-cipher-info.mjs create mode 100644 tests/corpus/7154-crypto-cipher-info.cjs create mode 100644 tests/corpus/7155-cipher-aes-ccm.cjs create mode 100644 tests/corpus/7156-cipher-key-wrap.cjs create mode 100644 tests/corpus/7157-crypto-cipher-info-typed.ts create mode 100644 tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json create mode 100644 tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json create mode 100644 tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json create mode 100644 tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 971065408..bd8e96880 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,9 @@ `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. - `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, CCM, ECB, CFB, CFB8, CFB1, and OFB modes and as key wrap (the wrap and wrap-pad ciphers and their -inv forms), des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `getCipherInfo` answers Node's info for every cipher OpenSSL names, including the ones without a native implementation. + The result is Node's object; the SIV ciphers and the SM4 GCM, CCM, and XTS ciphers have no `nid`, and reading it where the declared `CipherInfo` type gives `number` throws a TypeError. Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`. Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error. Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c341da852..c3f1835ab 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11660,9 +11660,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 9393fcaa8..ba8692b49 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19332,6 +19332,10 @@ "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs", + "tests/corpus/7154-crypto-cipher-info.cjs", + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", + "tests/corpus/7157-crypto-cipher-info-typed.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19719,6 +19723,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -19760,6 +19766,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -20434,8 +20442,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.getCipherInfo" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.getCipherInfo", + "tests": [ + "tests/corpus/7154-crypto-cipher-info.cjs", + "tests/corpus/7157-crypto-cipher-info-typed.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 06aad26cf..100882c1a 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -44,8 +44,8 @@ "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], "node-builtin.crypto.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], "node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], - "node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], - "node-builtin.crypto.createDecipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createCipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDecipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], "node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"], @@ -124,6 +124,7 @@ "node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"], "node-builtin.crypto.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], "node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.getCipherInfo": ["tests/corpus/7154-crypto-cipher-info.cjs", "tests/corpus/7157-crypto-cipher-info-typed.ts"], "node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"], "node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 5f3805813..b20eee94d 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3171,13 +3171,17 @@ declare module "crypto" { callback: (error: Error | null, buffer: Buffer) => void, ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; - /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, - * cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and - * chacha20-poly1305. Both are Transforms: written chunks go through - * update(), and the end pushes final(). */ + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb, + * cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede, + * chacha20, and chacha20-poly1305. Both are Transforms: written chunks go + * through update(), and the end pushes final(). */ + export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm"; export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; export type CipherKey = BinaryLike | KeyObject; + export interface CipherCCMOptions { + authTagLength: number; + } export interface CipherGCMOptions { authTagLength?: number; } @@ -3197,6 +3201,10 @@ declare module "crypto" { final(outputEncoding: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } + export interface CipherCCM extends Cipheriv { + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + getAuthTag(): Buffer; + } export interface CipherGCM extends Cipheriv { setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; getAuthTag(): Buffer; @@ -3218,6 +3226,10 @@ declare module "crypto" { final(outputEncoding: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } + export interface DecipherCCM extends Decipheriv { + setAuthTag(buffer: ArrayBufferView): this; + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + } export interface DecipherGCM extends Decipheriv { setAuthTag(buffer: ArrayBufferView): this; setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; @@ -3226,6 +3238,12 @@ declare module "crypto" { setAuthTag(buffer: ArrayBufferView): this; setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; } + export function createCipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): CipherCCM; export function createCipheriv( algorithm: CipherGCMTypes, key: CipherKey, @@ -3244,6 +3262,12 @@ declare module "crypto" { iv: BinaryLike | null, options?: { authTagLength?: number }, ): Cipheriv; + export function createDecipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): DecipherCCM; export function createDecipheriv( algorithm: CipherGCMTypes, key: CipherKey, @@ -3468,6 +3492,34 @@ declare module "crypto" { * per member like http2.constants. */ export function getFips(): 1 | 0; export function getCiphers(): string[]; + export type CipherMode = + | "cbc" + | "ccm" + | "cfb" + | "ctr" + | "ecb" + | "gcm" + | "ocb" + | "ofb" + | "stream" + | "wrap" + | "xts"; + export interface CipherInfoOptions { + keyLength?: number | undefined; + ivLength?: number | undefined; + } + export interface CipherInfo { + name: string; + nid: number; + blockSize?: number | undefined; + ivLength?: number | undefined; + keyLength: number; + mode: CipherMode; + } + export function getCipherInfo( + nameOrNid: string | number, + options?: CipherInfoOptions, + ): CipherInfo | undefined; export function getHashes(): string[]; export function getCurves(): string[]; /* Node v24 crypto.constants, literal-typed (the Http2Constants diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d0888ccbe..7d92b59f0 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -580,6 +580,7 @@ export const LIB_FN_SYMS: Record = { "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", "crypto.dhNew": "scr_crypto_dh_new", "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.getCipherInfo": "scr_crypto_get_cipher_info", "crypto.checkPrimeCb": "scr_crypto_check_prime_async", "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index bc9643670..6fb51fd32 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -72,6 +72,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_itable.c", "scr_slots.c", "scr_alloc.c", + "scr_cipher_info.c", ] as const; export function runtimeSrcDir(): string { diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 47e66f9b2..9ae1cf7f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -869,9 +869,17 @@ const IMPLEMENTED_CIPHERS = new Set([ "id-aes192-gcm", "id-aes256-gcm", "chacha20-poly1305", - ...["128", "192", "256"].flatMap((bits) => - ["ecb", "cfb", "cfb8", "cfb1", "ofb"].map((mode) => `aes-${bits}-${mode}`), - ), + ...["128", "192", "256"].flatMap((bits) => [ + ...["ecb", "cfb", "cfb8", "cfb1", "ofb", "ccm"].map((mode) => `aes-${bits}-${mode}`), + `id-aes${bits}-ccm`, + ...["wrap", "wrap-pad"].flatMap((wrap) => [ + `aes-${bits}-${wrap}`, + `aes${bits}-${wrap}`, + `id-aes${bits}-${wrap}`, + `aes-${bits}-${wrap}-inv`, + `aes${bits}-${wrap}-inv`, + ]), + ]), "des-ede3-cbc", "des3", "des-ede3", @@ -889,9 +897,10 @@ const IMPLEMENTED_CIPHERS = new Set([ ]); const SUPPORTED_CIPHERS_HINT = - "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 " + - "and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb " + - "and ofb modes, chacha20, and chacha20-poly1305"; + "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, " + + "cfb1 and ofb modes and their wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in " + + "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and " + + "chacha20-poly1305"; /** A literal cipher name that Node knows but the runtime does not * implement is refused at compile time. Unknown literal names keep Node's @@ -1380,10 +1389,11 @@ function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean } /** checkPrime, checkPrimeSync, generatePrime and generatePrimeSync - * (scr_dh.c): checked-dynamic arguments, which the runtime checks as Node - * does (a function in the options position is the callback), and the - * sync forms' boolean, ArrayBuffer or bigint converted to the call site's - * type. Null for other members. */ + * (scr_dh.c), and getCipherInfo (scr_cipher_info.c): checked-dynamic + * arguments, which the runtime checks as Node does (a function in the + * options position is the callback), and the sync forms' boolean, + * ArrayBuffer, bigint or info object converted to the call site's type. + * Null for other members. */ function lowerPrimeCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -1395,6 +1405,7 @@ function lowerPrimeCall( checkPrimeSync: ["crypto.checkPrimeSync", 1, 2, "checkPrimeSync(candidate[, options])"], checkPrime: ["crypto.checkPrimeCb", 1, 3, "checkPrime(candidate[, options], callback)"], generatePrimeSync: ["crypto.generatePrimeSync", 1, 2, "generatePrimeSync(size[, options])"], + getCipherInfo: ["crypto.getCipherInfo", 0, 2, "getCipherInfo(nameOrNid[, options])"], generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], }; const form = forms[member]; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index cb16fd257..d16b30e34 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1593,6 +1593,9 @@ export const BUILTIN_MODULE_FNS: Record< // Prime checks and generation (lowerPrimeCall): checked-dynamic // arguments, results converted to the call site's type. checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + // Checked-dynamic arguments, the result converted to the call site's + // type (lowerPrimeCall). + getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, @@ -3101,14 +3104,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< diffieHellman: "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", - ...Object.fromEntries( - ["getCipherInfo"].map((m) => [ - m, - "cipher metadata has no lowering — createCipheriv and createDecipheriv support " + - "aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and " + - "des-ede, chacha20, and chacha20-poly1305", - ]), - ), 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 14379f30f..9b57fa7a4 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4074,6 +4074,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, order) }; } + // crypto.getCipherInfo's result is Node's info object itself, read as a + // checked-dynamic value: Node leaves out the fields a cipher lacks, + // including the nid the declaration requires, so a record cannot hold + // every result. + if ( + isStdlibInterface("CipherInfo") && + psym!.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "crypto"), + ) + ) + return DYN; // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 7d01bb83d..8738238e8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // Node's prime, generator, size, encoding and group-name errors. "crypto.dhNew", "crypto.dhGroupNew", + // Node's nameOrNid and options errors. + "crypto.getCipherInfo", // Node's candidate, size, options, add and rem errors, and OpenSSL's // generation errors. "crypto.checkPrimeSync", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6bf864da6..0afebe32f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2840,6 +2840,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * undefined, and call it in a later macrotask; Node's argument errors * throw (may-throw). */ "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments + * (scr_cipher_info.c): the info object or undefined. */ + "crypto.getCipherInfo": { argTypes: [DYN, DYN], result: DYN }, "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 81fb23ae2..e44f5d31e 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -612,9 +612,7 @@ "id": "node-builtin.crypto.getCipherInfo", "kind": "node-builtin", "name": "crypto.getCipherInfo", - "status": "unsupported", - "code": "SC2020", - "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and chacha20-poly1305" + "status": "static" }, { "id": "node-builtin.crypto.getDiffieHellman", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json new file mode 100644 index 000000000..65e5f162e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7154-crypto-cipher-info.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json new file mode 100644 index 000000000..a52818c74 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7155-cipher-aes-ccm.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json new file mode 100644 index 000000000..f2473c303 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7156-cipher-key-wrap.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json new file mode 100644 index 000000000..340d6e7b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7157-crypto-cipher-info-typed.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 322832f9f..57610c80b 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -49,6 +49,8 @@ export const BASE_RUNTIME_SOURCES = [ "scr_itable.c", "scr_slots.c", "scr_alloc.c", + // crypto.getCipherInfo's table; it links only into programs that call it. + "scr_cipher_info.c", ]; export const OPTIONAL_RUNTIME_SOURCES = [ diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 00d229bad..2894af8e2 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -37,17 +37,23 @@ typedef enum { SC_CFB1, SC_OFB, SC_CHACHA20, + /* CCM and the key wraps stay last: sc_transform tests mode >= SC_CCM. */ + SC_CCM, + SC_WRAP, /* RFC 3394 AES key wrap */ + SC_WRAP_PAD, /* RFC 5649 AES key wrap with padding */ } ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; - bool des; /* triple DES (two- or three-key) rather than AES */ + bool des; /* triple DES (two- or three-key) rather than AES */ + bool inverse; /* key wrap with the inverse cipher (the -inv names) */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. - * Lookup is case-insensitive, like OpenSSL's. */ + * Lookup is case-insensitive, like OpenSSL's, and scans in order, so the + * CCM and key wrap names follow the others. */ static const ScCipherSpec sc_specs[] = { {"aes-128-cbc", SC_CBC, 16}, {"aes-192-cbc", SC_CBC, 24}, {"aes-256-cbc", SC_CBC, 32}, {"aes128", SC_CBC, 16}, {"aes192", SC_CBC, 24}, {"aes256", SC_CBC, 32}, @@ -68,6 +74,38 @@ static const ScCipherSpec sc_specs[] = { {"des-ede-ecb", SC_ECB, 16, true}, {"des-ede-cfb", SC_CFB, 16, true}, {"des-ede-ofb", SC_OFB, 16, true}, {"chacha20", SC_CHACHA20, 32}, + {"aes-128-ccm", SC_CCM, 16}, {"aes-192-ccm", SC_CCM, 24}, {"aes-256-ccm", SC_CCM, 32}, + {"id-aes128-ccm", SC_CCM, 16}, {"id-aes192-ccm", SC_CCM, 24}, {"id-aes256-ccm", SC_CCM, 32}, + {"aes-128-wrap", SC_WRAP, 16, false, false}, + {"aes128-wrap", SC_WRAP, 16, false, false}, + {"id-aes128-wrap", SC_WRAP, 16, false, false}, + {"aes-128-wrap-inv", SC_WRAP, 16, false, true}, + {"aes128-wrap-inv", SC_WRAP, 16, false, true}, + {"aes-128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"id-aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"aes-128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, + {"aes128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, + {"aes-192-wrap", SC_WRAP, 24, false, false}, + {"aes192-wrap", SC_WRAP, 24, false, false}, + {"id-aes192-wrap", SC_WRAP, 24, false, false}, + {"aes-192-wrap-inv", SC_WRAP, 24, false, true}, + {"aes192-wrap-inv", SC_WRAP, 24, false, true}, + {"aes-192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"id-aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"aes-192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, + {"aes192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, + {"aes-256-wrap", SC_WRAP, 32, false, false}, + {"aes256-wrap", SC_WRAP, 32, false, false}, + {"id-aes256-wrap", SC_WRAP, 32, false, false}, + {"aes-256-wrap-inv", SC_WRAP, 32, false, true}, + {"aes256-wrap-inv", SC_WRAP, 32, false, true}, + {"aes-256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"id-aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"aes-256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, + {"aes256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, }; /* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not @@ -139,7 +177,8 @@ typedef struct ScCipher { int tag_state; unsigned char tag[16]; unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: - shift register; chacha20: the IV */ + shift register; chacha20, CCM: the IV; key wrap: + the integrity check value */ unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream offset */ @@ -152,6 +191,18 @@ typedef struct ScCipher { uint64_t aad_len; /* chacha20-poly1305 AAD bytes */ uint64_t text_len; /* chacha20-poly1305 data bytes */ bool aad_open; /* AAD was added since the last data or padding */ + /* CCM: OpenSSL's ccm128 state (the counter block, whose first byte holds + * the L and M parameters and the Adata flag, and the CBC-MAC), its + * provider's flags, and Node's one-update rule. A decipher's expected + * tag is `tag`. */ + size_t ccm_iv_len; + unsigned char ccm_nonce[16]; + unsigned char ccm_cmac[16]; + bool ccm_iv_set, ccm_len_set, ccm_tag_set; + bool ccm_updated; /* a decipher's one update() ran; key wrap: the one update() */ + bool ccm_auth_failed; /* that update failed to authenticate */ + double ccm_max_message; + bool wrap_decrypt_block; /* key wrap: the AES key schedule decrypts */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ double pending; /* the decoder's carried bytes */ /* The Transform of a cipher used as a stream (+1) and its release @@ -513,6 +564,10 @@ static size_t sc_iv_length(ScCipherMode mode, bool des) { return 12; case SC_CHACHA20: return 16; + case SC_WRAP: + return 8; + case SC_WRAP_PAD: + return 4; default: return des ? 8 : 16; } @@ -530,9 +585,9 @@ static void sc_unsupported_cipher(const ScrStr *name) { scr_jb_puts(&b, "The cipher '"); scr_jb_put_str(&b, name); scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128, aes-192 and " - "aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, " - "cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " - "chacha20-poly1305"); + "aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, " + "wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 " + "and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and chacha20-poly1305"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -618,7 +673,10 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } size_t expected_iv = sc_iv_length(spec->mode, spec->des); - if (spec->mode == SC_GCM ? iv_len == 0 : iv_len != expected_iv || (expected_iv > 0 && !iv_bytes)) { + bool iv_ok = spec->mode == SC_GCM ? iv_len > 0 + : spec->mode == SC_CCM ? iv_len >= 7 && iv_len <= 13 + : iv_len == expected_iv && (expected_iv == 0 || iv_bytes); + if (!iv_ok) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -646,6 +704,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * sc_invalid_tag((unsigned long)tag_option); goto done; } + } else if (spec->mode == SC_CCM) { + /* InitAuthenticated: CCM needs the tag length, which OpenSSL takes when + * it is even and 4 to 16 bytes. */ + if (tag_option < 0) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "authTagLength required for "); + scr_jb_put_str(&b, cipher->v.str); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_CRYPTO_INVALID_AUTH_TAG"); + scr_str_release(message); + goto done; + } + if (tag_option < 4 || tag_option > 16 || (unsigned long)tag_option % 2 != 0) { + sc_invalid_tag((unsigned long)tag_option); + goto done; + } + tag_len = (int)tag_option; } if (key_bytes->len != spec->key_len) { sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Invalid key length"); @@ -709,6 +785,29 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); memcpy(c->block, iv_bytes->data, 16); break; + case SC_WRAP: + case SC_WRAP_PAD: + /* SP 800-38F: wrapping is the forward transformation (encryption) and + * unwrapping the inverse, and the -inv ciphers swap the two. The IV is + * the wrap's integrity check value. */ + rc = decrypt != spec->inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + c->wrap_decrypt_block = decrypt != spec->inverse; + memcpy(c->block, iv_bytes->data, iv_len); + break; + case SC_CCM: { + /* CRYPTO_ccm128_init with M = the tag length and L = 15 - the IV + * length; Node caps the message at what a 12- or 13-byte IV's length + * field holds, and at INT_MAX otherwise. */ + rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + unsigned L = 15 - (unsigned)iv_len; + c->ccm_nonce[0] = (unsigned char)(((L - 1) & 7) | ((((unsigned)tag_len - 2) / 2) & 7) << 3); + memcpy(c->block, iv_bytes->data, iv_len); + c->ccm_iv_len = iv_len; + c->ccm_iv_set = true; + c->ccm_max_message = iv_len == 13 ? 65535 : iv_len == 12 ? 16777215 : 2147483647; + break; + } case SC_GCM: rc = mbedtls_gcm_setkey(&c->gcm, MBEDTLS_CIPHER_ID_AES, key_bytes->data, bits); if (rc == 0) @@ -970,7 +1069,12 @@ static bool sc_chacha_finish(ScCipher *c, unsigned char tag[16]) { /* The cipher output of an update over len bytes (+1 Buffer), or NULL * after a backend failure (for example AES-GCM's 2^36 - 32 byte message * limit) has retired the context and thrown Node's update error. */ +static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len); +static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len); + static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) { + /* CCM and the key wraps, the last modes, take their own update. */ + if (c->mode >= SC_CCM) return c->mode == SC_CCM ? sc_ccm_update(c, in, len) : sc_wrap_update(c, in, len); size_t room = len + 16; ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)room); out->is_buffer = true; @@ -1006,6 +1110,10 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha20_update(c, in, len, out->data); produced = len; break; + case SC_CCM: + case SC_WRAP: + case SC_WRAP_PAD: + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -1019,6 +1127,376 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) return out; } +/* ── CCM ────────────────────────────────────────────────────────────── + * OpenSSL's ccm128 (crypto/modes/ccm128.c) over the AES block, step for + * step, with its provider's flags (ciphercommon_ccm.c) and Node's rules + * (one update, the message-length cap, a decipher's failure reported by + * final()), so every call sequence answers as Node's does. */ + +static void sc_ccm_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { + mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out); +} + +/* CRYPTO_ccm128_setiv: the nonce and the message length. */ +static void sc_ccm_setiv(ScCipher *c, uint64_t mlen) { + unsigned L = c->ccm_nonce[0] & 7; + if (L >= 3) { + c->ccm_nonce[8] = (unsigned char)(mlen >> 56); + c->ccm_nonce[9] = (unsigned char)(mlen >> 48); + c->ccm_nonce[10] = (unsigned char)(mlen >> 40); + c->ccm_nonce[11] = (unsigned char)(mlen >> 32); + } else { + memset(c->ccm_nonce + 8, 0, 8); + } + c->ccm_nonce[12] = (unsigned char)(mlen >> 24); + c->ccm_nonce[13] = (unsigned char)(mlen >> 16); + c->ccm_nonce[14] = (unsigned char)(mlen >> 8); + c->ccm_nonce[15] = (unsigned char)mlen; + c->ccm_nonce[0] &= (unsigned char)~0x40; + memcpy(c->ccm_nonce + 1, c->block, 14 - L); +} + +/* CRYPTO_ccm128_aad. */ +static void sc_ccm_aad(ScCipher *c, const unsigned char *aad, size_t alen) { + if (alen == 0) return; + c->ccm_nonce[0] |= 0x40; + sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); + size_t i; + if (alen < 0x10000 - 0x100) { + c->ccm_cmac[0] ^= (unsigned char)(alen >> 8); + c->ccm_cmac[1] ^= (unsigned char)alen; + i = 2; + } else if ((uint64_t)alen >= (uint64_t)1 << 32) { + c->ccm_cmac[0] ^= 0xFF; + c->ccm_cmac[1] ^= 0xFF; + for (int k = 0; k < 8; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (56 - 8 * k)); + i = 10; + } else { + c->ccm_cmac[0] ^= 0xFF; + c->ccm_cmac[1] ^= 0xFE; + for (int k = 0; k < 4; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (24 - 8 * k)); + i = 6; + } + do { + for (; i < 16 && alen; ++i, ++aad, --alen) c->ccm_cmac[i] ^= *aad; + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + i = 0; + } while (alen); +} + +/* ctr64_inc: the counter block's low eight bytes, big-endian. */ +static void sc_ccm_ctr_inc(unsigned char *counter) { + for (int n = 15; n >= 8; n--) + if (++counter[n]) return; +} + +/* CRYPTO_ccm128_encrypt and CRYPTO_ccm128_decrypt: false (-1) for a + * length that differs from the one the counter block holds. */ +static bool sc_ccm_crypt(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len, bool decrypt) { + unsigned char flags0 = c->ccm_nonce[0]; + unsigned char scratch[16]; + if (!(flags0 & 0x40)) sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); + unsigned L = flags0 & 7; + c->ccm_nonce[0] = (unsigned char)L; + uint64_t n = 0; + for (unsigned i = 15 - L; i < 15; ++i) { + n |= c->ccm_nonce[i]; + c->ccm_nonce[i] = 0; + n <<= 8; + } + n |= c->ccm_nonce[15]; + c->ccm_nonce[15] = 1; + if (n != (uint64_t)len) return false; + /* Each full block runs the counter block's encryption before the + * CBC-MAC's, in both directions, and XORs in 64-bit words, so the two + * AES operations of a block overlap. `out` may be `in`. */ + while (len >= 16) { + uint64_t data[2], mac[2], pad[2]; + sc_ccm_block(c, c->ccm_nonce, scratch); + sc_ccm_ctr_inc(c->ccm_nonce); + memcpy(data, in, 16); + memcpy(mac, c->ccm_cmac, 16); + memcpy(pad, scratch, 16); + pad[0] ^= data[0]; + pad[1] ^= data[1]; + /* The MAC covers the plaintext: the input when encrypting, the output + * when decrypting. */ + mac[0] ^= decrypt ? pad[0] : data[0]; + mac[1] ^= decrypt ? pad[1] : data[1]; + memcpy(out, pad, 16); + memcpy(c->ccm_cmac, mac, 16); + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + in += 16; + out += 16; + len -= 16; + } + if (len) { + if (decrypt) { + sc_ccm_block(c, c->ccm_nonce, scratch); + for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= (out[i] = (unsigned char)(scratch[i] ^ in[i])); + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + } else { + for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= in[i]; + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + sc_ccm_block(c, c->ccm_nonce, scratch); + for (size_t i = 0; i < len; ++i) out[i] = (unsigned char)(scratch[i] ^ in[i]); + } + } + for (unsigned i = 15 - L; i < 16; ++i) c->ccm_nonce[i] = 0; + sc_ccm_block(c, c->ccm_nonce, scratch); + for (int i = 0; i < 16; i++) c->ccm_cmac[i] ^= scratch[i]; + c->ccm_nonce[0] = flags0; + mbedtls_platform_zeroize(scratch, sizeof scratch); + return true; +} + +/* CRYPTO_ccm128_tag: the MAC, when the counter block still holds the + * tag length M (a failed call clears it). */ +static bool sc_ccm_tag(const ScCipher *c, unsigned char *tag) { + unsigned M = ((c->ccm_nonce[0] >> 3) & 7) * 2 + 2; + if (M != (unsigned)c->tag_len) return false; + memcpy(tag, c->ccm_cmac, M); + return true; +} + +/* The provider's ccm_cipher_internal for data (`out` given): the length + * is set by the first data unless setAAD set it; encryption marks the tag + * computed; decryption needs the tag and checks it, and on success clears + * the flags so a second call fails. */ +static bool sc_ccm_data(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len) { + if (!c->ccm_iv_set) return false; + if (!c->ccm_len_set) { + sc_ccm_setiv(c, len); + c->ccm_len_set = true; + } + if (!c->decrypt) { + if (!sc_ccm_crypt(c, in, out, len, false)) return false; + c->ccm_tag_set = true; + return true; + } + if (!c->ccm_tag_set) return false; + unsigned char computed[16]; + bool ok = sc_ccm_crypt(c, in, out, len, true) && sc_ccm_tag(c, computed) && + sc_tag_equal(computed, c->tag, (size_t)c->tag_len); + mbedtls_platform_zeroize(computed, sizeof computed); + if (!ok) { + if (len) mbedtls_platform_zeroize(out, len); + return false; + } + c->ccm_iv_set = c->ccm_tag_set = c->ccm_len_set = false; + return true; +} + +static bool sc_ccm_length_ok(const ScCipher *c, double len) { + if (len <= c->ccm_max_message) return true; + sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_MESSAGELEN", "Invalid message length"); + return false; +} + +/* update(): a decipher takes one, and its authentication failure leaves + * empty output for final() to report. Node passes empty data to OpenSSL + * without a buffer, which the provider skips. */ +static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) { + if (!sc_ccm_length_ok(c, (double)len)) return NULL; + if (c->decrypt && c->ccm_updated) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len ? len : 1)); + out->is_buffer = true; + bool ok = len == 0 || sc_ccm_data(c, in, out->data, len); + if (c->decrypt) { + c->ccm_updated = true; + if (!ok) c->ccm_auth_failed = true; + out->len = ok ? len : 0; + return out; + } + if (!ok) { + mbedtls_platform_zeroize(out->data, len); + scr_bytes_release(out); + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + out->len = len; + return out; +} + +/* final(): a decipher reports the update's verdict; a cipher runs the + * provider's final (an empty message when no data was encrypted) and + * reads the tag, which retires the provider's state. */ +static bool sc_ccm_final(ScCipher *c) { + if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_updated && !c->ccm_auth_failed; + unsigned char dummy = 0, sink = 0; + if (c->ccm_iv_set && !c->ccm_tag_set && !sc_ccm_data(c, &dummy, &sink, 0)) return false; + if (!c->ccm_tag_set || !sc_ccm_tag(c, c->tag)) return false; + c->tag_state = SC_TAG_COMPUTED; + c->ccm_tag_set = c->ccm_iv_set = c->ccm_len_set = false; + return true; +} + +/* setAAD(buffer, { plaintextLength }): the length goes first, as Node + * passes it to OpenSSL, then the AAD. False when the provider refuses. */ +static bool sc_ccm_set_aad(ScCipher *c, const ScrBytes *aad, double plaintext_length) { + if (!c->ccm_iv_set) return false; + sc_ccm_setiv(c, (uint64_t)plaintext_length); + c->ccm_len_set = true; + sc_ccm_aad(c, aad->data, aad->len); + return true; +} + +/* ── key wrap ───────────────────────────────────────────────────────── + * OpenSSL's CRYPTO_128_wrap, CRYPTO_128_unwrap and their RFC 5649 + * padded forms (crypto/modes/wrap128.c) over the key's AES transform, and + * Node's one-update rule: a failed update() leaves the cipher usable, an + * empty one counts, and final() without one fails. */ + +static void sc_wrap_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { + mbedtls_aes_crypt_ecb(&c->aes, c->wrap_decrypt_block ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, in, out); +} + +static void sc_wrap_xor_t(unsigned char *a, size_t t) { + a[7] ^= (unsigned char)(t & 0xff); + if (t > 0xff) { + a[6] ^= (unsigned char)((t >> 8) & 0xff); + a[5] ^= (unsigned char)((t >> 16) & 0xff); + a[4] ^= (unsigned char)((t >> 24) & 0xff); + } +} + +/* CRYPTO_128_wrap: 0 unless the input is at least 16 bytes in 8-byte + * blocks. `out` may be `in`. */ +static size_t sc_wrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { + if ((inlen & 7) || inlen < 16) return 0; + unsigned char b[16]; + size_t t = 1; + memmove(out + 8, in, inlen); + memcpy(b, iv, 8); + for (int j = 0; j < 6; j++) { + unsigned char *r = out + 8; + for (size_t i = 0; i < inlen; i += 8, t++, r += 8) { + memcpy(b + 8, r, 8); + sc_wrap_block(c, b, b); + sc_wrap_xor_t(b, t); + memcpy(r, b + 8, 8); + } + } + memcpy(out, b, 8); + mbedtls_platform_zeroize(b, sizeof b); + return inlen + 8; +} + +/* crypto_128_unwrap_raw: the plaintext into `out` and the recovered + * integrity check value into `aiv`. */ +static size_t sc_unwrap_raw(ScCipher *c, unsigned char aiv[8], unsigned char *out, const unsigned char *in, size_t inlen) { + inlen -= 8; + if ((inlen & 7) || inlen < 16) return 0; + unsigned char b[16]; + size_t t = 6 * (inlen >> 3); + memcpy(b, in, 8); + memmove(out, in + 8, inlen); + for (int j = 0; j < 6; j++) { + unsigned char *r = out + inlen - 8; + for (size_t i = 0; i < inlen; i += 8, t--, r -= 8) { + sc_wrap_xor_t(b, t); + memcpy(b + 8, r, 8); + sc_wrap_block(c, b, b); + memcpy(r, b + 8, 8); + } + } + memcpy(aiv, b, 8); + mbedtls_platform_zeroize(b, sizeof b); + return inlen; +} + +static size_t sc_unwrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { + unsigned char got[8]; + size_t ret = sc_unwrap_raw(c, got, out, in, inlen); + if (!ret) return 0; + if (!sc_tag_equal(got, iv, 8)) { + mbedtls_platform_zeroize(out, ret); + return 0; + } + return ret; +} + +/* CRYPTO_128_wrap_pad: the 4-byte ICV and the 32-bit length, then the + * data padded with zeros to 8 bytes; one block is a single encryption. */ +static size_t sc_wrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { + size_t padded = (inlen + 7) / 8 * 8; + if (inlen == 0 || inlen >= ((size_t)1 << 31)) return 0; + unsigned char aiv[8]; + memcpy(aiv, icv, 4); + aiv[4] = (unsigned char)((inlen >> 24) & 0xFF); + aiv[5] = (unsigned char)((inlen >> 16) & 0xFF); + aiv[6] = (unsigned char)((inlen >> 8) & 0xFF); + aiv[7] = (unsigned char)(inlen & 0xFF); + if (padded == 8) { + memmove(out + 8, in, inlen); + memcpy(out, aiv, 8); + memset(out + 8 + inlen, 0, padded - inlen); + sc_wrap_block(c, out, out); + return 16; + } + memmove(out, in, inlen); + memset(out + inlen, 0, padded - inlen); + return sc_wrap(c, aiv, out, out, padded); +} + +/* CRYPTO_128_unwrap_pad: the ICV, the length within the last 8 bytes and + * zero padding must all check. */ +static size_t sc_unwrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { + if ((inlen & 7) || inlen < 16) return 0; + size_t n = inlen / 8 - 1, padded; + unsigned char aiv[8]; + if (inlen == 16) { + unsigned char buff[16]; + sc_wrap_block(c, in, buff); + memcpy(aiv, buff, 8); + memcpy(out, buff + 8, 8); + padded = 8; + mbedtls_platform_zeroize(buff, sizeof buff); + } else { + padded = inlen - 8; + if (sc_unwrap_raw(c, aiv, out, in, inlen) != padded) { + mbedtls_platform_zeroize(out, inlen); + return 0; + } + } + size_t plain = (size_t)aiv[4] << 24 | (size_t)aiv[5] << 16 | (size_t)aiv[6] << 8 | aiv[7]; + static const unsigned char zeros[8] = {0}; + if (!sc_tag_equal(aiv, icv, 4) || 8 * (n - 1) >= plain || plain > 8 * n || + !sc_tag_equal(out + plain, zeros, padded - plain)) { + mbedtls_platform_zeroize(out, padded); + return 0; + } + return plain; +} + +static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len) { + if (c->ccm_updated) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len + 16)); + out->is_buffer = true; + size_t produced = 0; + if (len > 0) { + bool pad = c->mode == SC_WRAP_PAD; + produced = !c->decrypt ? (pad ? sc_wrap_pad(c, c->block, out->data, in, len) : sc_wrap(c, c->block, out->data, in, len)) + : (pad ? sc_unwrap_pad(c, c->block, out->data, in, len) + : sc_unwrap(c, c->block, out->data, in, len)); + if (produced == 0) { + mbedtls_platform_zeroize(out->data, len + 16); + scr_bytes_release(out); + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + } + c->ccm_updated = true; + out->len = produced; + return out; +} + /* ── output encodings ─────────────────────────────────────────────────── */ /* Node's getDecoder: the StringDecoder for this encoding, created on first @@ -1131,7 +1609,7 @@ static ScrDyn *sc_update(ScCipher *c, ScrDyn *data, ScrDyn *input_encoding, ScrD } static bool sc_aead(const ScCipher *c) { - return c->mode == SC_GCM || c->mode == SC_CHACHA; + return c->mode == SC_GCM || c->mode == SC_CHACHA || c->mode == SC_CCM; } /* final()'s output bytes (+1 Buffer), or NULL with the error thrown. */ @@ -1164,6 +1642,13 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { case SC_OFB: case SC_CHACHA20: break; + case SC_CCM: + ok = sc_ccm_final(c); + break; + case SC_WRAP: + case SC_WRAP_PAD: + backend = c->ccm_updated; + break; case SC_GCM: { unsigned char full[16]; size_t ignored; @@ -1231,6 +1716,20 @@ static ScrDyn *sc_set_aad(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *opt ScrBytes *aad = sc_buffer_or_view(buffer, "aadbuf", encoding); scr_dyn_release(encoding); if (!aad) return NULL; + if (c->mode == SC_CCM && c->active) { + bool ok = true; + if (plaintext_length < 0) { + sc_throw(SCR_ERR_TYPE, "ERR_MISSING_ARGS", "options.plaintextLength required for CCM mode with AAD"); + ok = false; + } else if (!sc_ccm_length_ok(c, plaintext_length)) { + ok = false; + } else if (!sc_ccm_set_aad(c, aad, plaintext_length)) { + sc_invalid_state("setAAD"); + ok = false; + } + scr_bytes_release(aad); + return ok ? scr_dyn_retain(self) : NULL; + } /* After data, GCM refuses any AAD and chacha20-poly1305 refuses * non-empty AAD. */ bool ok = c->active && sc_aead(c) && !(c->data_started && (c->mode == SC_GCM || aad->len > 0)); @@ -1314,6 +1813,7 @@ static ScrDyn *sc_set_auth_tag(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn c->tag_len = (int)len; memcpy(c->tag, tag->data, len); c->tag_state = SC_TAG_SET_BY_USER; + if (c->mode == SC_CCM) c->ccm_tag_set = true; scr_bytes_release(tag); return scr_dyn_retain(self); } diff --git a/packages/runtime/src/scr_cipher_info.c b/packages/runtime/src/scr_cipher_info.c new file mode 100644 index 000000000..8264f4e87 --- /dev/null +++ b/packages/runtime/src/scr_cipher_info.c @@ -0,0 +1,161 @@ +/* scriptc runtime — crypto.getCipherInfo(nameOrNid[, options]) over the + * table Node's OpenSSL ciphers generate (scr_cipher_info.h): cipher.js's + * argument checks, then GetCipherInfo's keyLength and ivLength rules and + * its info object, in Node's key order. Names match case-insensitively; + * nids are OpenSSL's. Arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_cipher_info.h" + +#define CI_COUNT(table) (sizeof(table) / sizeof((table)[0])) + +/* validateInt32(value, name): false with Node's error pending. */ +static bool ci_validate_int32(const char *name, const ScrDyn *value) { + if (value->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail(name, "of type number", value); + return false; + } + double v = value->v.num; + bool integer = isfinite(v) && trunc(v) == v; + if (integer && v >= INT32_MIN && v <= INT32_MAX) return true; + char received[48]; + scr_num_received(v, received); + char text[192]; + int n = integer ? snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. " + "Received %s", + name, received) + : snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be an integer. Received %s", name, + received); + scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* A plain view of a statically typed argument (+1), or NULL after a + * throw. */ +static ScrDyn *ci_plain(ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(value); + return scr_dyn_retain(value); +} + +static const ScCipherInfo *ci_by_name(const ScrStr *name) { + char lower[64]; + if (name->len == 0 || name->len >= sizeof lower) return NULL; + for (size_t i = 0; i < name->len; i++) { + unsigned char ch = (unsigned char)name->data[i]; + lower[i] = (char)(ch >= 'A' && ch <= 'Z' ? ch + 32 : ch); + } + lower[name->len] = '\0'; + size_t lo = 0, hi = CI_COUNT(sc_cipher_info_names); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int cmp = strcmp(sc_cipher_info_names[mid].name, lower); + if (cmp == 0) return &sc_cipher_infos[sc_cipher_info_names[mid].info]; + if (cmp < 0) lo = mid + 1; + else hi = mid; + } + return NULL; +} + +static const ScCipherInfo *ci_by_nid(double nid) { + size_t lo = 0, hi = CI_COUNT(sc_cipher_info_nids); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + double at = sc_cipher_info_nids[mid].nid; + if (at == nid) return &sc_cipher_infos[sc_cipher_info_nids[mid].info]; + if (at < nid) lo = mid + 1; + else hi = mid; + } + return NULL; +} + +/* GetCipherInfo's option rules: a cipher that takes neither option (the + * legacy provider's) answers undefined for either; keyLength must equal + * the cipher's; ivLength must equal its IV length, or lie in 7 to 13 for + * CCM and 1 to 15 for OCB, and GCM takes any. */ +static bool ci_accepts(const ScCipherInfo *info, bool has_key, int key, bool has_iv, int iv) { + if ((has_key || has_iv) && (info->key_rule == SC_INFO_NONE || info->iv_rule == SC_INFO_NONE)) return false; + if (has_key && key != info->key_length) return false; + if (!has_iv) return true; + switch (info->iv_rule) { + case SC_INFO_CCM: return iv >= 7 && iv <= 13; + case SC_INFO_OCB: return iv >= 1 && iv <= 15; + case SC_INFO_ANY: return true; + default: return iv == (info->iv_length < 0 ? 0 : info->iv_length); + } +} + +static void ci_set_str(ScrDyn *obj, const char *key, const char *text) { + ScrStr *s = scr_str_new(text, strlen(text)); + ScrDyn *v = scr_dyn_new_str(s); + scr_str_release(s); + scr_dyn_obj_set(obj, key, strlen(key), v); +} + +static void ci_set_num(ScrDyn *obj, const char *key, double value) { + scr_dyn_obj_set(obj, key, strlen(key), scr_dyn_new_num(value)); +} + +/* The info object: mode, name, nid, blockSize (not for stream ciphers), + * ivLength (not when it is 0) and keyLength. */ +static ScrDyn *ci_object(const ScCipherInfo *info, int iv) { + ScrDyn *obj = scr_dyn_new_obj(); + if (info->mode) ci_set_str(obj, "mode", info->mode); + ci_set_str(obj, "name", info->name); + if (info->nid >= 0) ci_set_num(obj, "nid", info->nid); + if (info->block_size >= 0) ci_set_num(obj, "blockSize", info->block_size); + if (iv > 0) ci_set_num(obj, "ivLength", iv); + ci_set_num(obj, "keyLength", info->key_length); + return obj; +} + +static ScrDyn *ci_get(const ScrDyn *name_or_nid, const ScrDyn *options) { + if (name_or_nid->kind != SCR_DYN_STR && name_or_nid->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("nameOrNid", "one of type string or number", name_or_nid); + return NULL; + } + if (name_or_nid->kind == SCR_DYN_NUM && !ci_validate_int32("nameOrNid", name_or_nid)) return NULL; + bool has_key = false, has_iv = false; + int key = 0, iv = 0; + if (options->kind != SCR_DYN_UNDEF) { + ScrStr *type = scr_dyn_typeof(options); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (!object || options->kind == SCR_DYN_NULL || options->kind == SCR_DYN_ARR) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NULL; + } + const ScrDyn *key_value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, "keyLength", 9) : NULL; + const ScrDyn *iv_value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, "ivLength", 8) : NULL; + if (key_value && key_value->kind != SCR_DYN_UNDEF) { + if (!ci_validate_int32("options.keyLength", key_value)) return NULL; + has_key = true; + key = (int)key_value->v.num; + } + if (iv_value && iv_value->kind != SCR_DYN_UNDEF) { + if (!ci_validate_int32("options.ivLength", iv_value)) return NULL; + has_iv = true; + iv = (int)iv_value->v.num; + } + } + const ScCipherInfo *info = + name_or_nid->kind == SCR_DYN_STR ? ci_by_name(name_or_nid->v.str) : ci_by_nid(name_or_nid->v.num); + if (!info || !ci_accepts(info, has_key, key, has_iv, iv)) return scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); + return ci_object(info, has_iv ? iv : info->iv_length); +} + +ScrDyn *scr_crypto_get_cipher_info(ScrDyn *name_or_nid, ScrDyn *options) { + ScrDyn *name = ci_plain(name_or_nid); + if (!name) return NULL; + ScrDyn *opts = ci_plain(options); + ScrDyn *result = opts ? ci_get(name, opts) : NULL; + if (opts) scr_dyn_release(opts); + scr_dyn_release(name); + return result; +} diff --git a/packages/runtime/src/scr_cipher_info.h b/packages/runtime/src/scr_cipher_info.h new file mode 100644 index 000000000..edb18caea --- /dev/null +++ b/packages/runtime/src/scr_cipher_info.h @@ -0,0 +1,615 @@ +/* Generated by scripts/gen-cipher-info.mjs from Node v24.22.0's + * crypto.getCipherInfo: every cipher OpenSSL names, with the fields Node + * reports (-1 or NULL for a field it leaves out) and the keyLength and + * ivLength options each accepts. Do not edit. */ +#ifndef SCR_CIPHER_INFO_H +#define SCR_CIPHER_INFO_H + +enum { SC_INFO_NONE, SC_INFO_EXACT, SC_INFO_CCM, SC_INFO_OCB, SC_INFO_ANY }; + +typedef struct { + const char *mode; + const char *name; + int nid; + int block_size; + int iv_length; + int key_length; + unsigned char key_rule; + unsigned char iv_rule; +} ScCipherInfo; + +static const ScCipherInfo sc_cipher_infos[] = { + {"ecb", "camellia-128-ecb", 754, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "camellia-192-ecb", 755, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-192-ofb", 767, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb", 758, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-192-ctr", 967, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-128-ofb", 766, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb", 757, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "camellia-256-ecb", 756, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-256-ofb", 768, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb", 759, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-256-ctr", 971, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-128-ctr", 963, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "sm4-ecb", 1133, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "sm4-cbc", 1134, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "sm4-ofb", 1135, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "sm4-cfb", 1137, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "sm4-ctr", 1139, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-128-cbc", 751, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-192-cbc", 752, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-256-cbc", 753, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-128-ecb", 1065, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-192-ctr", 1074, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-256-ecb", 1075, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-256-cbc", 1076, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb", 1077, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aria-256-ofb", 1078, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-256-ctr", 1079, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-128-cbc", 1066, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb", 1067, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "aria-128-gcm", 1123, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"gcm", "aria-192-gcm", 1124, 1, 12, 24, SC_INFO_EXACT, SC_INFO_ANY}, + {"gcm", "aria-256-gcm", 1125, 1, 12, 32, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "aria-128-ccm", 1120, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"ccm", "aria-192-ccm", 1121, 1, 12, 24, SC_INFO_EXACT, SC_INFO_CCM}, + {"ccm", "aria-256-ccm", 1122, 1, 12, 32, SC_INFO_EXACT, SC_INFO_CCM}, + {"ofb", "aria-128-ofb", 1068, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-128-ctr", 1069, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-192-ecb", 1070, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-192-cbc", 1071, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb", 1072, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aria-192-ofb", 1073, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-smime-alg-cms3deswrap", 246, 8, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "des-ede3-cbc", 44, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"xts", "aes-128-xts", 913, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"xts", "aes-256-xts", 914, 1, 16, 64, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "des-ede", 32, 8, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-128-ecb", 418, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-128-cbc", 419, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-192-ecb", 422, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-192-cbc", 423, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-192-ofb", 424, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb", 425, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes192-wrap", 789, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes192-gcm", 898, 1, 12, 24, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes192-ccm", 899, 1, 12, 24, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes192-wrap-pad", 900, 8, 4, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-128-ofb", 420, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb", 421, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-256-ecb", 426, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-256-cbc", 427, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-256-ofb", 428, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb", 429, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes256-wrap", 790, 8, 8, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes256-gcm", 901, 1, 12, 32, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes256-ccm", 902, 1, 12, 32, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes256-wrap-pad", 903, 8, 4, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes128-wrap", 788, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes128-gcm", 895, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes128-ccm", 896, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes128-wrap-pad", 897, 8, 4, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-128-cbc-cts", -1, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb1", 650, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb8", 653, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-128-ctr", 904, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-128-gcm-siv", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-128-ocb", 958, 16, 12, 16, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-128-siv", -1, 1, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-128-wrap-inv", -1, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-128-wrap-pad-inv", -1, 8, 4, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-192-cbc-cts", -1, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb1", 651, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb8", 654, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-192-ctr", 905, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-192-gcm-siv", -1, 1, 12, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-192-ocb", 959, 16, 12, 24, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-192-siv", -1, 1, -1, 48, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-192-wrap-inv", -1, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-192-wrap-pad-inv", -1, 8, 4, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-256-cbc-cts", -1, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb1", 652, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb8", 655, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-256-ctr", 906, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-256-gcm-siv", -1, 1, 12, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-256-ocb", 960, 16, 12, 32, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-256-siv", -1, 1, -1, 64, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-256-wrap-inv", -1, 8, 8, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-256-wrap-pad-inv", -1, 8, 4, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb1", 1080, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb8", 1083, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb1", 1081, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb8", 1084, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb1", 1082, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb8", 1085, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "bf-cbc", 91, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "bf-cfb", 93, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "bf-ecb", 92, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "bf-ofb", 94, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "camellia-128-cbc-cts", -1, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb1", 760, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb8", 763, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-192-cbc-cts", -1, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb1", 761, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb8", 764, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-256-cbc-cts", -1, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb1", 762, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb8", 765, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "cast5-cbc", 108, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "cast5-cfb", 110, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "cast5-ecb", 109, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "cast5-ofb", 111, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "chacha20", 1019, -1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"stream", "chacha20-poly1305", 1018, -1, 12, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "des-cbc", 31, 8, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb", 30, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb1", 656, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb8", 657, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "des-ecb", 29, 8, -1, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "des-ede-cbc", 43, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede-cfb", 60, 1, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ede-ofb", 62, 1, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "des-ede3", 33, 8, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb", 61, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb1", 658, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb8", 659, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ede3-ofb", 63, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ofb", 45, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "desx-cbc", 80, 8, 8, 24, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "idea-cbc", 34, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "idea-cfb", 35, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "idea-ecb", 36, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "idea-ofb", 46, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-cbc", 37, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-40-cbc", 98, 8, 8, 5, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-64-cbc", 166, 8, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "rc2-cfb", 39, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "rc2-ecb", 38, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "rc2-ofb", 40, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4", 5, -1, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4-40", 97, -1, -1, 5, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4-hmac-md5", 915, -1, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "seed-cbc", 777, 16, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "seed-cfb", 779, 1, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "seed-ecb", 776, 16, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "seed-ofb", 778, 1, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ccm", "sm4-ccm", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"gcm", "sm4-gcm", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"xts", "sm4-xts", -1, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, +}; + +/* Lowercase names, sorted, with their record. */ +static const struct { + const char *name; + unsigned short info; +} sc_cipher_info_names[] = { + {"0.3.4401.5.3.1.9.1", 0}, + {"0.3.4401.5.3.1.9.21", 1}, + {"0.3.4401.5.3.1.9.23", 2}, + {"0.3.4401.5.3.1.9.24", 3}, + {"0.3.4401.5.3.1.9.29", 4}, + {"0.3.4401.5.3.1.9.3", 5}, + {"0.3.4401.5.3.1.9.4", 6}, + {"0.3.4401.5.3.1.9.41", 7}, + {"0.3.4401.5.3.1.9.43", 8}, + {"0.3.4401.5.3.1.9.44", 9}, + {"0.3.4401.5.3.1.9.49", 10}, + {"0.3.4401.5.3.1.9.9", 11}, + {"1.2.156.10197.1.104.1", 12}, + {"1.2.156.10197.1.104.2", 13}, + {"1.2.156.10197.1.104.3", 14}, + {"1.2.156.10197.1.104.4", 15}, + {"1.2.156.10197.1.104.7", 16}, + {"1.2.392.200011.61.1.1.1.2", 17}, + {"1.2.392.200011.61.1.1.1.3", 18}, + {"1.2.392.200011.61.1.1.1.4", 19}, + {"1.2.410.200046.1.1.1", 20}, + {"1.2.410.200046.1.1.10", 21}, + {"1.2.410.200046.1.1.11", 22}, + {"1.2.410.200046.1.1.12", 23}, + {"1.2.410.200046.1.1.13", 24}, + {"1.2.410.200046.1.1.14", 25}, + {"1.2.410.200046.1.1.15", 26}, + {"1.2.410.200046.1.1.2", 27}, + {"1.2.410.200046.1.1.3", 28}, + {"1.2.410.200046.1.1.34", 29}, + {"1.2.410.200046.1.1.35", 30}, + {"1.2.410.200046.1.1.36", 31}, + {"1.2.410.200046.1.1.37", 32}, + {"1.2.410.200046.1.1.38", 33}, + {"1.2.410.200046.1.1.39", 34}, + {"1.2.410.200046.1.1.4", 35}, + {"1.2.410.200046.1.1.5", 36}, + {"1.2.410.200046.1.1.6", 37}, + {"1.2.410.200046.1.1.7", 38}, + {"1.2.410.200046.1.1.8", 39}, + {"1.2.410.200046.1.1.9", 40}, + {"1.2.840.113549.1.9.16.3.6", 41}, + {"1.2.840.113549.3.7", 42}, + {"1.3.111.2.1619.0.1.1", 43}, + {"1.3.111.2.1619.0.1.2", 44}, + {"1.3.14.3.2.17", 45}, + {"2.16.840.1.101.3.4.1.1", 46}, + {"2.16.840.1.101.3.4.1.2", 47}, + {"2.16.840.1.101.3.4.1.21", 48}, + {"2.16.840.1.101.3.4.1.22", 49}, + {"2.16.840.1.101.3.4.1.23", 50}, + {"2.16.840.1.101.3.4.1.24", 51}, + {"2.16.840.1.101.3.4.1.25", 52}, + {"2.16.840.1.101.3.4.1.26", 53}, + {"2.16.840.1.101.3.4.1.27", 54}, + {"2.16.840.1.101.3.4.1.28", 55}, + {"2.16.840.1.101.3.4.1.3", 56}, + {"2.16.840.1.101.3.4.1.4", 57}, + {"2.16.840.1.101.3.4.1.41", 58}, + {"2.16.840.1.101.3.4.1.42", 59}, + {"2.16.840.1.101.3.4.1.43", 60}, + {"2.16.840.1.101.3.4.1.44", 61}, + {"2.16.840.1.101.3.4.1.45", 62}, + {"2.16.840.1.101.3.4.1.46", 63}, + {"2.16.840.1.101.3.4.1.47", 64}, + {"2.16.840.1.101.3.4.1.48", 65}, + {"2.16.840.1.101.3.4.1.5", 66}, + {"2.16.840.1.101.3.4.1.6", 67}, + {"2.16.840.1.101.3.4.1.7", 68}, + {"2.16.840.1.101.3.4.1.8", 69}, + {"aes-128-cbc", 47}, + {"aes-128-cbc-cts", 70}, + {"aes-128-ccm", 68}, + {"aes-128-cfb", 57}, + {"aes-128-cfb1", 71}, + {"aes-128-cfb8", 72}, + {"aes-128-ctr", 73}, + {"aes-128-ecb", 46}, + {"aes-128-gcm", 67}, + {"aes-128-gcm-siv", 74}, + {"aes-128-ocb", 75}, + {"aes-128-ofb", 56}, + {"aes-128-siv", 76}, + {"aes-128-wrap", 66}, + {"aes-128-wrap-inv", 77}, + {"aes-128-wrap-pad", 69}, + {"aes-128-wrap-pad-inv", 78}, + {"aes-128-xts", 43}, + {"aes-192-cbc", 49}, + {"aes-192-cbc-cts", 79}, + {"aes-192-ccm", 54}, + {"aes-192-cfb", 51}, + {"aes-192-cfb1", 80}, + {"aes-192-cfb8", 81}, + {"aes-192-ctr", 82}, + {"aes-192-ecb", 48}, + {"aes-192-gcm", 53}, + {"aes-192-gcm-siv", 83}, + {"aes-192-ocb", 84}, + {"aes-192-ofb", 50}, + {"aes-192-siv", 85}, + {"aes-192-wrap", 52}, + {"aes-192-wrap-inv", 86}, + {"aes-192-wrap-pad", 55}, + {"aes-192-wrap-pad-inv", 87}, + {"aes-256-cbc", 59}, + {"aes-256-cbc-cts", 88}, + {"aes-256-ccm", 64}, + {"aes-256-cfb", 61}, + {"aes-256-cfb1", 89}, + {"aes-256-cfb8", 90}, + {"aes-256-ctr", 91}, + {"aes-256-ecb", 58}, + {"aes-256-gcm", 63}, + {"aes-256-gcm-siv", 92}, + {"aes-256-ocb", 93}, + {"aes-256-ofb", 60}, + {"aes-256-siv", 94}, + {"aes-256-wrap", 62}, + {"aes-256-wrap-inv", 95}, + {"aes-256-wrap-pad", 65}, + {"aes-256-wrap-pad-inv", 96}, + {"aes-256-xts", 44}, + {"aes128", 47}, + {"aes128-wrap", 66}, + {"aes128-wrap-inv", 77}, + {"aes128-wrap-pad", 69}, + {"aes128-wrap-pad-inv", 78}, + {"aes192", 49}, + {"aes192-wrap", 52}, + {"aes192-wrap-inv", 86}, + {"aes192-wrap-pad", 55}, + {"aes192-wrap-pad-inv", 87}, + {"aes256", 59}, + {"aes256-wrap", 62}, + {"aes256-wrap-inv", 95}, + {"aes256-wrap-pad", 65}, + {"aes256-wrap-pad-inv", 96}, + {"aria-128-cbc", 27}, + {"aria-128-ccm", 32}, + {"aria-128-cfb", 28}, + {"aria-128-cfb1", 97}, + {"aria-128-cfb8", 98}, + {"aria-128-ctr", 36}, + {"aria-128-ecb", 20}, + {"aria-128-gcm", 29}, + {"aria-128-ofb", 35}, + {"aria-192-cbc", 38}, + {"aria-192-ccm", 33}, + {"aria-192-cfb", 39}, + {"aria-192-cfb1", 99}, + {"aria-192-cfb8", 100}, + {"aria-192-ctr", 21}, + {"aria-192-ecb", 37}, + {"aria-192-gcm", 30}, + {"aria-192-ofb", 40}, + {"aria-256-cbc", 23}, + {"aria-256-ccm", 34}, + {"aria-256-cfb", 24}, + {"aria-256-cfb1", 101}, + {"aria-256-cfb8", 102}, + {"aria-256-ctr", 26}, + {"aria-256-ecb", 22}, + {"aria-256-gcm", 31}, + {"aria-256-ofb", 25}, + {"aria128", 27}, + {"aria192", 38}, + {"aria256", 23}, + {"bf", 103}, + {"bf-cbc", 103}, + {"bf-cfb", 104}, + {"bf-ecb", 105}, + {"bf-ofb", 106}, + {"blowfish", 103}, + {"camellia-128-cbc", 17}, + {"camellia-128-cbc-cts", 107}, + {"camellia-128-cfb", 6}, + {"camellia-128-cfb1", 108}, + {"camellia-128-cfb8", 109}, + {"camellia-128-ctr", 11}, + {"camellia-128-ecb", 0}, + {"camellia-128-ofb", 5}, + {"camellia-192-cbc", 18}, + {"camellia-192-cbc-cts", 110}, + {"camellia-192-cfb", 3}, + {"camellia-192-cfb1", 111}, + {"camellia-192-cfb8", 112}, + {"camellia-192-ctr", 4}, + {"camellia-192-ecb", 1}, + {"camellia-192-ofb", 2}, + {"camellia-256-cbc", 19}, + {"camellia-256-cbc-cts", 113}, + {"camellia-256-cfb", 9}, + {"camellia-256-cfb1", 114}, + {"camellia-256-cfb8", 115}, + {"camellia-256-ctr", 10}, + {"camellia-256-ecb", 7}, + {"camellia-256-ofb", 8}, + {"camellia128", 17}, + {"camellia192", 18}, + {"camellia256", 19}, + {"cast", 116}, + {"cast-cbc", 116}, + {"cast5-cbc", 116}, + {"cast5-cfb", 117}, + {"cast5-ecb", 118}, + {"cast5-ofb", 119}, + {"chacha20", 120}, + {"chacha20-poly1305", 121}, + {"des", 122}, + {"des-cbc", 122}, + {"des-cfb", 123}, + {"des-cfb1", 124}, + {"des-cfb8", 125}, + {"des-ecb", 126}, + {"des-ede", 45}, + {"des-ede-cbc", 127}, + {"des-ede-cfb", 128}, + {"des-ede-ecb", 45}, + {"des-ede-ofb", 129}, + {"des-ede3", 130}, + {"des-ede3-cbc", 42}, + {"des-ede3-cfb", 131}, + {"des-ede3-cfb1", 132}, + {"des-ede3-cfb8", 133}, + {"des-ede3-ecb", 130}, + {"des-ede3-ofb", 134}, + {"des-ofb", 135}, + {"des3", 42}, + {"des3-wrap", 41}, + {"desx", 136}, + {"desx-cbc", 136}, + {"id-aes128-ccm", 68}, + {"id-aes128-gcm", 67}, + {"id-aes128-wrap", 66}, + {"id-aes128-wrap-pad", 69}, + {"id-aes192-ccm", 54}, + {"id-aes192-gcm", 53}, + {"id-aes192-wrap", 52}, + {"id-aes192-wrap-pad", 55}, + {"id-aes256-ccm", 64}, + {"id-aes256-gcm", 63}, + {"id-aes256-wrap", 62}, + {"id-aes256-wrap-pad", 65}, + {"id-smime-alg-cms3deswrap", 41}, + {"idea-cbc", 137}, + {"idea-cfb", 138}, + {"idea-ecb", 139}, + {"idea-ofb", 140}, + {"rc2", 141}, + {"rc2-128", 141}, + {"rc2-40", 142}, + {"rc2-40-cbc", 142}, + {"rc2-64", 143}, + {"rc2-64-cbc", 143}, + {"rc2-cbc", 141}, + {"rc2-cfb", 144}, + {"rc2-ecb", 145}, + {"rc2-ofb", 146}, + {"rc4", 147}, + {"rc4-40", 148}, + {"rc4-hmac-md5", 149}, + {"seed", 150}, + {"seed-cbc", 150}, + {"seed-cfb", 151}, + {"seed-ecb", 152}, + {"seed-ofb", 153}, + {"sm4", 13}, + {"sm4-cbc", 13}, + {"sm4-ccm", 154}, + {"sm4-cfb", 15}, + {"sm4-cfb128", 15}, + {"sm4-ctr", 16}, + {"sm4-ecb", 12}, + {"sm4-gcm", 155}, + {"sm4-ofb", 14}, + {"sm4-ofb128", 14}, + {"sm4-xts", 156}, +}; + +/* Nids, ascending, with their record. */ +static const struct { + unsigned short nid; + unsigned short info; +} sc_cipher_info_nids[] = { + {5, 147}, + {29, 126}, + {30, 123}, + {31, 122}, + {32, 45}, + {33, 130}, + {34, 137}, + {35, 138}, + {36, 139}, + {37, 141}, + {38, 145}, + {39, 144}, + {40, 146}, + {43, 127}, + {44, 42}, + {45, 135}, + {46, 140}, + {60, 128}, + {61, 131}, + {62, 129}, + {63, 134}, + {80, 136}, + {91, 103}, + {92, 105}, + {93, 104}, + {94, 106}, + {97, 148}, + {98, 142}, + {108, 116}, + {109, 118}, + {110, 117}, + {111, 119}, + {166, 143}, + {246, 41}, + {418, 46}, + {419, 47}, + {420, 56}, + {421, 57}, + {422, 48}, + {423, 49}, + {424, 50}, + {425, 51}, + {426, 58}, + {427, 59}, + {428, 60}, + {429, 61}, + {650, 71}, + {651, 80}, + {652, 89}, + {653, 72}, + {654, 81}, + {655, 90}, + {656, 124}, + {657, 125}, + {658, 132}, + {659, 133}, + {751, 17}, + {752, 18}, + {753, 19}, + {754, 0}, + {755, 1}, + {756, 7}, + {757, 6}, + {758, 3}, + {759, 9}, + {760, 108}, + {761, 111}, + {762, 114}, + {763, 109}, + {764, 112}, + {765, 115}, + {766, 5}, + {767, 2}, + {768, 8}, + {776, 152}, + {777, 150}, + {778, 153}, + {779, 151}, + {788, 66}, + {789, 52}, + {790, 62}, + {895, 67}, + {896, 68}, + {897, 69}, + {898, 53}, + {899, 54}, + {900, 55}, + {901, 63}, + {902, 64}, + {903, 65}, + {904, 73}, + {905, 82}, + {906, 91}, + {913, 43}, + {914, 44}, + {915, 149}, + {958, 75}, + {959, 84}, + {960, 93}, + {963, 11}, + {967, 4}, + {971, 10}, + {1018, 121}, + {1019, 120}, + {1065, 20}, + {1066, 27}, + {1067, 28}, + {1068, 35}, + {1069, 36}, + {1070, 37}, + {1071, 38}, + {1072, 39}, + {1073, 40}, + {1074, 21}, + {1075, 22}, + {1076, 23}, + {1077, 24}, + {1078, 25}, + {1079, 26}, + {1080, 97}, + {1081, 99}, + {1082, 101}, + {1083, 98}, + {1084, 100}, + {1085, 102}, + {1120, 32}, + {1121, 33}, + {1122, 34}, + {1123, 29}, + {1124, 30}, + {1125, 31}, + {1133, 12}, + {1134, 13}, + {1135, 14}, + {1137, 15}, + {1139, 16}, + {1198, 76}, + {1199, 85}, + {1200, 94}, + {1248, 155}, + {1249, 154}, + {1290, 156}, +}; + +#endif diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 0e6c6e07f..f61d27406 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -108,6 +108,9 @@ ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, * Node's error pending. */ ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding); ScrDyn *scr_crypto_dh_group_new(ScrDyn *name); +/* crypto.getCipherInfo(nameOrNid, options) (scr_cipher_info.c): the info + * object or undefined (+1), or NULL with Node's argument error pending. */ +ScrDyn *scr_crypto_get_cipher_info(ScrDyn *name_or_nid, ScrDyn *options); /* crypto.checkPrimeSync, checkPrime, generatePrimeSync and generatePrime * (scr_dh.c): checked-dynamic arguments, borrowed; the sync forms answer * +1 or NULL with the error pending. The callbacks receive undefined as diff --git a/scripts/gen-cipher-info.mjs b/scripts/gen-cipher-info.mjs new file mode 100644 index 000000000..06bd8bce3 --- /dev/null +++ b/scripts/gen-cipher-info.mjs @@ -0,0 +1,125 @@ +// Writes packages/runtime/src/scr_cipher_info.h from the running Node's +// crypto.getCipherInfo: one record per cipher, the names (lowercase) and nids +// that find it, and the keyLength and ivLength options each accepts. The +// candidate names are getCiphers() and `openssl list -cipher-algorithms` +// (OpenSSL's aliases and its legacy-provider ciphers, which getCiphers() +// leaves out). Run with the pinned Node: node scripts/gen-cipher-info.mjs +import crypto from "node:crypto"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; + +const listed = execFileSync("openssl", ["list", "-cipher-algorithms"], { encoding: "utf8" }) + .match(/[A-Za-z0-9][A-Za-z0-9._-]*/g) + .filter((word) => !["Legacy", "Provided", "default", "legacy", "base", "fips"].includes(word)); +const candidates = new Set([...crypto.getCiphers(), ...listed]); +for (let nid = 1; nid < 1400; nid++) { + const info = crypto.getCipherInfo(nid); + if (info) candidates.add(info.name); +} + +function ivRule(name, info) { + const accepted = []; + for (let v = 0; v <= 70; v++) if (crypto.getCipherInfo(name, { ivLength: v })) accepted.push(v); + const large = !!crypto.getCipherInfo(name, { ivLength: 100000 }); + const own = info.ivLength ?? 0; + if (accepted.length === 0) return "SC_INFO_NONE"; + if (accepted.length === 1 && accepted[0] === own && !large) return "SC_INFO_EXACT"; + if (accepted.join() === "7,8,9,10,11,12,13" && !large) return "SC_INFO_CCM"; + if (accepted.join() === [...Array(15)].map((_, i) => i + 1).join() && !large) + return "SC_INFO_OCB"; + if (accepted.length === 71 && large) return "SC_INFO_ANY"; + throw new Error(`unclassified ivLength rule for ${name}: ${accepted}`); +} +function keyRule(name, info) { + const accepted = []; + for (let k = 0; k <= 80; k++) if (crypto.getCipherInfo(name, { keyLength: k })) accepted.push(k); + if (accepted.length === 0) return "SC_INFO_NONE"; + if (accepted.length === 1 && accepted[0] === info.keyLength) return "SC_INFO_EXACT"; + throw new Error(`unclassified keyLength rule for ${name}: ${accepted}`); +} + +const records = []; +const recordIndex = new Map(); +function recordOf(name, info) { + const key = JSON.stringify(info); + let index = recordIndex.get(key); + const rules = [keyRule(name, info), ivRule(name, info)]; + if (index === undefined) { + index = records.length; + records.push({ info, rules }); + recordIndex.set(key, index); + } else if (records[index].rules.join() !== rules.join()) { + throw new Error(`rules differ for ${name}`); + } + return index; +} + +const names = []; +for (const name of [...candidates].map((n) => n.toLowerCase()).sort()) { + if (names.length && names.at(-1)[0] === name) continue; + const info = crypto.getCipherInfo(name); + if (info) names.push([name, recordOf(name, info)]); +} +const nids = []; +for (let nid = 1; nid < 1400; nid++) { + const info = crypto.getCipherInfo(nid); + if (info) nids.push([nid, recordOf(info.name, info)]); +} + +const str = (s) => (s === undefined ? "NULL" : JSON.stringify(s)); +const num = (n) => (n === undefined ? -1 : n); +let out = `/* Generated by scripts/gen-cipher-info.mjs from Node ${process.version}'s + * crypto.getCipherInfo: every cipher OpenSSL names, with the fields Node + * reports (-1 or NULL for a field it leaves out) and the keyLength and + * ivLength options each accepts. Do not edit. */ +#ifndef SCR_CIPHER_INFO_H +#define SCR_CIPHER_INFO_H + +enum { SC_INFO_NONE, SC_INFO_EXACT, SC_INFO_CCM, SC_INFO_OCB, SC_INFO_ANY }; + +typedef struct { + const char *mode; + const char *name; + int nid; + int block_size; + int iv_length; + int key_length; + unsigned char key_rule; + unsigned char iv_rule; +} ScCipherInfo; + +static const ScCipherInfo sc_cipher_infos[] = { +`; +for (const { info, rules } of records) + out += ` {${str(info.mode)}, ${str(info.name)}, ${num(info.nid)}, ${num(info.blockSize)}, ${num(info.ivLength)}, ${info.keyLength}, ${rules[0]}, ${rules[1]}},\n`; +out += `}; + +/* Lowercase names, sorted, with their record. */ +static const struct { + const char *name; + unsigned short info; +} sc_cipher_info_names[] = { +`; +for (const [name, index] of names) out += ` {${JSON.stringify(name)}, ${index}},\n`; +out += `}; + +/* Nids, ascending, with their record. */ +static const struct { + unsigned short nid; + unsigned short info; +} sc_cipher_info_nids[] = { +`; +for (const [nid, index] of nids) out += ` {${nid}, ${index}},\n`; +out += `}; + +#endif +`; +for (const { info } of records) { + const keys = Object.keys(info).join(); + const expected = ["mode", "name", "nid", "blockSize", "ivLength", "keyLength"] + .filter((k) => k in info) + .join(); + if (keys !== expected) throw new Error(`unexpected key order ${keys}`); +} +fs.writeFileSync(new URL("../packages/runtime/src/scr_cipher_info.h", import.meta.url), out); +console.log(records.length, "records,", names.length, "names,", nids.length, "nids"); diff --git a/tests/corpus/7154-crypto-cipher-info.cjs b/tests/corpus/7154-crypto-cipher-info.cjs new file mode 100644 index 000000000..95e72f71d --- /dev/null +++ b/tests/corpus/7154-crypto-cipher-info.cjs @@ -0,0 +1,57 @@ +// crypto.getCipherInfo: every getCiphers() name, OpenSSL's aliases in any +// case and its legacy-provider ciphers, lookups by nid, the keyLength and +// ivLength options (exact lengths, CCM's 7 to 13 bytes, OCB's 1 to 15 and +// any length for GCM; the legacy ciphers answer undefined for either), +// and Node's argument errors. +const crypto = require("crypto"); + +for (const name of crypto.getCiphers()) console.log(name, JSON.stringify(crypto.getCipherInfo(name))); +for (const name of ["AES-128-CBC", "Aes256", "id-aes128-CCM", "chacha20-POLY1305", "des", "bf", "bf-cbc", "rc4", "seed-cbc", "cast5-cbc", "rc2-cbc", "idea-cbc", "des-ecb", "desx", "nope", ""]) + console.log(JSON.stringify(name), JSON.stringify(crypto.getCipherInfo(name))); +for (const nid of [0, -1, 1, 5, 31, 91, 419, 427, 895, 896, 901, 1018, 1198, 1248, 99999]) + console.log("nid", nid, JSON.stringify(crypto.getCipherInfo(nid))); + +const options = [ + { keyLength: 16 }, + { keyLength: 32 }, + { ivLength: 16 }, + { ivLength: 12 }, + { ivLength: 7 }, + { ivLength: 13 }, + { ivLength: 14 }, + { ivLength: 0 }, + { ivLength: 1 }, + { ivLength: 15 }, + { ivLength: 1000 }, + { keyLength: 16, ivLength: 12 }, + { keyLength: undefined, ivLength: undefined }, + {}, +]; +for (const name of ["aes-128-cbc", "aes-128-gcm", "aes-128-ccm", "aes-128-ocb", "aes-128-ecb", "chacha20-poly1305", "aes-128-wrap-pad", "bf-cbc", "rc4"]) + for (const option of options) console.log(name, JSON.stringify(option), JSON.stringify(crypto.getCipherInfo(name, option))); + +function attempt(label, run) { + try { + console.log(label, "ok", JSON.stringify(run())); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getCipherInfo()); +attempt("null", () => crypto.getCipherInfo(null)); +attempt("boolean", () => crypto.getCipherInfo(true)); +attempt("object", () => crypto.getCipherInfo({})); +attempt("fraction", () => crypto.getCipherInfo(1.5)); +attempt("NaN", () => crypto.getCipherInfo(NaN)); +attempt("2^31", () => crypto.getCipherInfo(2 ** 31)); +attempt("options null", () => crypto.getCipherInfo("aes-128-cbc", null)); +attempt("options number", () => crypto.getCipherInfo("aes-128-cbc", 5)); +attempt("options string", () => crypto.getCipherInfo("aes-128-cbc", "x")); +attempt("options array", () => crypto.getCipherInfo("aes-128-cbc", [])); +attempt("keyLength string", () => crypto.getCipherInfo("aes-128-cbc", { keyLength: "16" })); +attempt("keyLength fraction", () => crypto.getCipherInfo("aes-128-cbc", { keyLength: 16.5 })); +attempt("ivLength 2^31", () => crypto.getCipherInfo("aes-128-cbc", { ivLength: 2 ** 31 })); +attempt("ivLength null", () => crypto.getCipherInfo("aes-128-cbc", { ivLength: null })); +attempt("unknown name with bad options", () => crypto.getCipherInfo("nope", { keyLength: "x" })); +const info = crypto.getCipherInfo("aes-256-gcm", { ivLength: 7 }); +console.log(Object.keys(info).join(), Object.getPrototypeOf(info) === Object.prototype); diff --git a/tests/corpus/7155-cipher-aes-ccm.cjs b/tests/corpus/7155-cipher-aes-ccm.cjs new file mode 100644 index 000000000..2ed95c382 --- /dev/null +++ b/tests/corpus/7155-cipher-aes-ccm.cjs @@ -0,0 +1,158 @@ +// AES-CCM through createCipheriv and createDecipheriv: RFC 3610 and NIST +// SP 800-38C vectors, every tag and IV length, AAD with plaintextLength, +// the message-length caps of 12- and 13-byte IVs, a decipher's failed +// authentication reported by final(), the one-update rule, the AAD and +// tag calls in each state, and as a stream. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const v = run(); + console.log(label, "ok", Buffer.isBuffer(v) ? v.toString("hex") : v); + } catch (e) { + console.log(label, e.name, e.code, JSON.stringify(e.message), JSON.stringify(Object.keys(e))); + } +} +const hex = (s) => Buffer.from(s, "hex"); + +// NIST SP 800-38C examples C.1 to C.3. +const nistKey = hex("404142434445464748494a4b4c4d4e4f"); +for (const [iv, aad, pt, tagLen] of [ + ["10111213141516", "0001020304050607", "20212223", 4], + ["1011121314151617", "000102030405060708090a0b0c0d0e0f", "202122232425262728292a2b2c2d2e2f", 6], + ["101112131415161718191a1b", "000102030405060708090a0b0c0d0e0f10111213", "202122232425262728292a2b2c2d2e2f3031323334353637", 8], +]) { + attempt(`nist iv ${iv.length / 2}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", nistKey, hex(iv), { authTagLength: tagLen }); + c.setAAD(hex(aad), { plaintextLength: pt.length / 2 }); + return Buffer.concat([c.update(hex(pt)), c.final(), c.getAuthTag()]); + }); +} +// RFC 3610 packet vector 1. +attempt("rfc3610 #1", () => { + const c = crypto.createCipheriv("aes-128-ccm", hex("c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"), hex("00000003020100a0a1a2a3a4a5"), { authTagLength: 8 }); + c.setAAD(hex("0001020304050607"), { plaintextLength: 23 }); + return Buffer.concat([c.update(hex("08090a0b0c0d0e0f101112131415161718191a1b1c1d1e")), c.final(), c.getAuthTag()]); +}); + +const key = Buffer.alloc(16, 1); +const pt = Buffer.from("hello world, ccm mode, three blocks of it!"); +for (const size of [16, 24, 32]) + for (const name of [`aes-${size * 8}-ccm`, `id-aes${size * 8}-ccm`, `AES-${size * 8}-CCM`]) + attempt(name, () => { + const c = crypto.createCipheriv(name, Buffer.alloc(size, 3), Buffer.alloc(12, 2), { authTagLength: 10 }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +for (const tagLen of [2, 3, 4, 5, 6, 8, 10, 12, 14, 15, 16, 18]) + attempt(`tag ${tagLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: tagLen }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +for (const ivLen of [0, 6, 7, 8, 11, 12, 13, 14]) + attempt(`iv ${ivLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(ivLen, 9), { authTagLength: 16 }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +attempt("no tag length", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12))); +attempt("no tag length, mixed case", () => crypto.createDecipheriv("Aes-128-Ccm", key, Buffer.alloc(12))); +attempt("tag length string", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: "16" })); +attempt("bad iv and no tag length", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(5))); +attempt("bad key", () => crypto.createCipheriv("aes-128-ccm", Buffer.alloc(15), Buffer.alloc(12), { authTagLength: 16 })); + +const iv = Buffer.alloc(12, 2), aad = Buffer.from("header"); +function seal(steps) { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const parts = []; + for (const step of steps) { + try { + const out = step(c); + if (Buffer.isBuffer(out)) parts.push(out.toString("hex")); + } catch (e) { + parts.push(`<${e.code ?? e.name}: ${e.message}>`); + } + } + try { + parts.push("tag " + c.getAuthTag().toString("hex")); + } catch (e) { + parts.push(`<${e.code}: ${e.message}>`); + } + return parts.join(" "); +} +const update = (data) => (c) => c.update(data); +const final = () => (c) => c.final(); +const setAAD = (data, options) => (c) => (c.setAAD(data, options), Buffer.alloc(0)); +console.log("plain", seal([update(pt), final()])); +console.log("aad", seal([setAAD(aad, { plaintextLength: pt.length }), update(pt), final()])); +console.log("aad without length", seal([setAAD(aad), update(pt), final()])); +console.log("aad bad length", seal([setAAD(aad, { plaintextLength: -1 }), update(pt), final()])); +console.log("aad length mismatch", seal([setAAD(aad, { plaintextLength: 5 }), update(pt), final()])); +console.log("aad twice", seal([setAAD(aad, { plaintextLength: pt.length }), setAAD(aad, { plaintextLength: pt.length }), update(pt), final()])); +console.log("empty aad", seal([setAAD(Buffer.alloc(0), { plaintextLength: pt.length }), update(pt), final()])); +console.log("aad after update", seal([update(pt), setAAD(aad, { plaintextLength: 3 }), final()])); +console.log("update twice", seal([update(pt), update(pt), final()])); +console.log("update empty twice", seal([update(Buffer.alloc(0)), update(Buffer.alloc(0)), final()])); +console.log("no update", seal([final()])); +console.log("aad, no update", seal([setAAD(aad, { plaintextLength: pt.length }), final()])); +console.log("aad length 0, no update", seal([setAAD(aad, { plaintextLength: 0 }), final()])); +console.log("tag before final", seal([update(pt)])); +console.log("final twice", seal([update(pt), final(), final()])); +console.log("update after final", seal([update(pt), final(), update(pt)])); +console.log("auto padding", seal([(c) => (c.setAutoPadding(false), Buffer.alloc(0)), update(pt), final()])); +console.log("strings", seal([(c) => Buffer.from(c.update("abcd", "utf8", "hex"), "hex"), (c) => Buffer.from(c.final("hex"), "hex")])); + +// Message-length caps: 2^16 - 1 bytes with a 13-byte IV, 2^24 - 1 with 12. +for (const [ivLen, size] of [[13, 65535], [13, 65536], [12, 65536]]) + attempt(`length ${size}, iv ${ivLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(ivLen), { authTagLength: 4 }); + return crypto.createHash("sha256").update(c.update(Buffer.alloc(size, 1))).digest("hex").slice(0, 16); + }); +attempt("aad length 65536, iv 13", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(13), { authTagLength: 4 }).setAAD(aad, { plaintextLength: 65536 })); +attempt("aad length 16777216, iv 12", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: 4 }).setAAD(aad, { plaintextLength: 16777216 })); + +const sealer = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); +sealer.setAAD(aad, { plaintextLength: pt.length }); +const ct = Buffer.concat([sealer.update(pt), sealer.final()]); +const tag = sealer.getAuthTag(); +function open(steps) { + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const parts = []; + for (const step of steps) { + try { + const out = step(d); + if (Buffer.isBuffer(out)) parts.push(JSON.stringify(out.toString())); + } catch (e) { + parts.push(`<${e.code ?? e.name}: ${e.message}>`); + } + } + return parts.join(" "); +} +const setAuthTag = (t) => (d) => (d.setAuthTag(t), Buffer.alloc(0)); +console.log("open", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open, tag after aad", open([setAAD(aad, { plaintextLength: ct.length }), setAuthTag(tag), update(ct), final()])); +console.log("open without tag", open([setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open bad tag", open([setAuthTag(Buffer.alloc(16)), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open without aad", open([setAuthTag(tag), update(ct), final()])); +console.log("open tampered", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(Buffer.concat([ct.subarray(0, 5), Buffer.from([ct[5] ^ 1]), ct.subarray(6)])), final()])); +console.log("open tag after update", open([setAAD(aad, { plaintextLength: ct.length }), update(ct), setAuthTag(tag), final()])); +console.log("open tag twice", open([setAuthTag(tag), setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open tag wrong length", open([setAuthTag(tag.subarray(0, 8))])); +console.log("open update twice", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), update(ct), final()])); +console.log("open final only", open([setAuthTag(tag), final()])); +console.log("open length mismatch", open([setAuthTag(tag), setAAD(aad, { plaintextLength: 3 }), update(ct), final()])); +console.log("open getAuthTag", open([(d) => d.getAuthTag()])); +console.log("open empty message", (() => { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); + c.update(Buffer.alloc(0)); + c.final(); + const t = c.getAuthTag(); + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); + d.setAuthTag(t); + return JSON.stringify(Buffer.concat([d.update(Buffer.alloc(0)), d.final()]).toString()); +})()); + +// As a stream: one write and the end. +const stream = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 12 }); +const chunks = []; +stream.on("data", (chunk) => chunks.push(chunk)); +stream.on("end", () => console.log("stream", Buffer.concat(chunks).toString("hex"), stream.getAuthTag().toString("hex"))); +stream.end(pt); diff --git a/tests/corpus/7156-cipher-key-wrap.cjs b/tests/corpus/7156-cipher-key-wrap.cjs new file mode 100644 index 000000000..a958d16c5 --- /dev/null +++ b/tests/corpus/7156-cipher-key-wrap.cjs @@ -0,0 +1,103 @@ +// The AES key-wrap ciphers through createCipheriv and createDecipheriv: +// RFC 3394 and RFC 5649 vectors under every name (aes-N-wrap, aesN-wrap, +// id-aesN-wrap, the -pad forms and the -inv ciphers), the input and IV +// lengths each takes, integrity failures, and the one-update rule. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const v = run(); + console.log(label, "ok", Buffer.isBuffer(v) ? v.toString("hex") : v); + } catch (e) { + console.log(label, e.name, e.code, JSON.stringify(e.message), JSON.stringify(Object.keys(e))); + } +} +const hex = (s) => Buffer.from(s, "hex"); +const wrap = (name, key, iv, input) => { + const c = crypto.createCipheriv(name, key, iv); + return Buffer.concat([c.update(input), c.final()]); +}; +const unwrap = (name, key, iv, input) => { + const d = crypto.createDecipheriv(name, key, iv); + return Buffer.concat([d.update(input), d.final()]); +}; +const iv8 = hex("a6a6a6a6a6a6a6a6"), iv4 = hex("a65959a6"); + +// RFC 3394 section 4. +const kek = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); +const keyData = hex("00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"); +for (const [kekBits, dataBits] of [[128, 128], [192, 128], [256, 128], [192, 192], [256, 192], [256, 256]]) { + const k = kek.subarray(0, kekBits / 8), d = keyData.subarray(0, dataBits / 8); + for (const name of [`aes-${kekBits}-wrap`, `aes${kekBits}-wrap`, `id-aes${kekBits}-wrap`, `ID-AES${kekBits}-WRAP`]) { + attempt(`${name} ${dataBits}`, () => wrap(name, k, iv8, d)); + attempt(`${name} ${dataBits} unwrap`, () => unwrap(name, k, iv8, wrap(name, k, iv8, d))); + } +} +// RFC 5649 section 6. +const kek5649 = hex("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"); +for (const data of ["c37b7e6492584340bed12207808941155068f738", "466f7250617369"]) + for (const name of ["aes-192-wrap-pad", "aes192-wrap-pad", "id-aes192-wrap-pad"]) { + attempt(`${name} ${data.length / 2}`, () => wrap(name, kek5649, iv4, hex(data))); + attempt(`${name} ${data.length / 2} unwrap`, () => unwrap(name, kek5649, iv4, wrap(name, kek5649, iv4, hex(data)))); + } + +const k128 = kek.subarray(0, 16); +for (const size of [0, 1, 7, 8, 9, 15, 16, 17, 24, 40]) { + attempt(`wrap ${size} bytes`, () => wrap("id-aes128-wrap", k128, iv8, Buffer.alloc(size, 5))); + attempt(`wrap-pad ${size} bytes`, () => { + const w = wrap("id-aes128-wrap-pad", k128, iv4, Buffer.alloc(size, 5)); + return w.toString("hex") + " -> " + unwrap("id-aes128-wrap-pad", k128, iv4, w).toString("hex"); + }); +} +for (const size of [8, 16, 20, 24, 32]) + for (const name of ["id-aes128-wrap", "id-aes128-wrap-pad"]) { + const iv = name.endsWith("pad") ? iv4 : iv8; + attempt(`${name} unwrap ${size} bytes`, () => unwrap(name, k128, iv, Buffer.alloc(size, 1))); + } +attempt("other iv", () => wrap("id-aes128-wrap", k128, Buffer.alloc(8, 1), keyData.subarray(0, 16))); +attempt("unwrap with another iv", () => unwrap("id-aes128-wrap", k128, Buffer.alloc(8, 1), wrap("id-aes128-wrap", k128, iv8, keyData.subarray(0, 16)))); +attempt("pad other iv", () => wrap("id-aes128-wrap-pad", k128, Buffer.alloc(4, 7), Buffer.from("key"))); +attempt("pad unwrap with another iv", () => unwrap("id-aes128-wrap-pad", k128, Buffer.alloc(4, 7), wrap("id-aes128-wrap-pad", k128, iv4, Buffer.from("key")))); +attempt("pad tampered length", () => { + const w = wrap("id-aes128-wrap-pad", k128, iv4, Buffer.alloc(20, 9)); + w[3] ^= 1; + return unwrap("id-aes128-wrap-pad", k128, iv4, w); +}); +for (const name of ["aes-128-wrap-inv", "aes128-wrap-inv", "aes-256-wrap-inv", "aes-128-wrap-pad-inv", "aes256-wrap-pad-inv"]) { + const bits = Number(name.match(/\d+/)[0]); + const iv = name.includes("pad") ? iv4 : iv8; + attempt(name, () => { + const w = wrap(name, kek.subarray(0, bits / 8), iv, keyData.subarray(0, 24)); + return w.toString("hex") + " -> " + unwrap(name, kek.subarray(0, bits / 8), iv, w).toString("hex"); + }); +} +attempt("null iv", () => crypto.createCipheriv("id-aes128-wrap", k128, null)); +attempt("iv 16", () => crypto.createCipheriv("id-aes128-wrap", k128, Buffer.alloc(16))); +attempt("pad iv 8", () => crypto.createCipheriv("id-aes128-wrap-pad", k128, iv8)); +attempt("key 24", () => crypto.createCipheriv("id-aes128-wrap", Buffer.alloc(24), iv8)); + +function steps(list) { + const c = crypto.createCipheriv("id-aes128-wrap", k128, iv8); + return list + .map((step) => { + try { + const out = step(c); + return Buffer.isBuffer(out) ? out.toString("hex") || '""' : String(out); + } catch (e) { + return `<${e.code ?? e.name}: ${e.message}>`; + } + }) + .join(" "); +} +const data = keyData.subarray(0, 16); +console.log("update twice", steps([(c) => c.update(data), (c) => c.update(data), (c) => c.final()])); +console.log("final only", steps([(c) => c.final()])); +console.log("final twice", steps([(c) => c.update(data), (c) => c.final(), (c) => c.final()])); +console.log("update after final", steps([(c) => c.update(data), (c) => c.final(), (c) => c.update(data)])); +console.log("bad then good", steps([(c) => c.update(Buffer.alloc(12)), (c) => c.update(data), (c) => c.final()])); +console.log("bad then final", steps([(c) => c.update(Buffer.alloc(12)), (c) => c.final()])); +console.log("empty then data", steps([(c) => c.update(Buffer.alloc(0)), (c) => c.update(data), (c) => c.final()])); +console.log("hex", steps([(c) => c.update(data, undefined, "hex"), (c) => c.final("hex")])); +console.log("auto padding", steps([(c) => c.setAutoPadding(false) === c, (c) => c.update(data)])); +console.log("setAAD", steps([(c) => c.setAAD(Buffer.alloc(1))])); +console.log("getAuthTag", steps([(c) => c.update(data), (c) => c.final(), (c) => c.getAuthTag()])); diff --git a/tests/corpus/7157-crypto-cipher-info-typed.ts b/tests/corpus/7157-crypto-cipher-info-typed.ts new file mode 100644 index 000000000..2de0f0069 --- /dev/null +++ b/tests/corpus/7157-crypto-cipher-info-typed.ts @@ -0,0 +1,42 @@ +// getCipherInfo, AES-CCM and AES key wrap through their declared types: the +// CipherInfo record's fields, absent fields and key order, the keyLength and +// ivLength options, and the CipherCCM and DecipherCCM members. +import { createCipheriv, createDecipheriv, getCipherInfo, type CipherInfo } from "node:crypto"; + +const info: CipherInfo | undefined = getCipherInfo("aes-128-ccm"); +if (info) console.log(info.name, info.nid, info.mode, info.keyLength, info.ivLength, info.blockSize); +const byNid = getCipherInfo(901); +console.log(byNid?.name, byNid?.mode); +const sized = getCipherInfo("aes-128-ccm", { ivLength: 7 }); +console.log(sized?.ivLength, getCipherInfo("aes-128-ccm", { ivLength: 6 })); +console.log(getCipherInfo("nope")); +console.log(JSON.stringify(getCipherInfo("AES-256-WRAP-PAD")!)); +console.log(Object.keys(getCipherInfo("chacha20")!), JSON.stringify(getCipherInfo("aes-128-ecb")!)); +for (const name of ["rc4", "des-ede3-cbc", "aes-256-xts", "chacha20-poly1305"]) { + const found = getCipherInfo(name, { keyLength: 32 }); + console.log(name, found ? Object.keys(found).join() : "none", found?.blockSize, found?.ivLength); +} + +const key = Buffer.alloc(16, 1); +const iv = Buffer.alloc(12, 2); +const aad = Buffer.from("header"); +const plain = Buffer.from("typed ccm message"); +const cipher = createCipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); +cipher.setAAD(aad, { plaintextLength: plain.length }); +const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); +const tag: Buffer = cipher.getAuthTag(); +console.log(ciphertext.toString("hex"), tag.toString("hex")); +const decipher = createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); +decipher.setAuthTag(tag); +decipher.setAAD(aad, { plaintextLength: ciphertext.length }); +const opened: Buffer = decipher.update(ciphertext); +decipher.final(); +console.log(opened.toString()); + +const kek = Buffer.alloc(24, 3); +const icv = Buffer.from("A6A6A6A6A6A6A6A6", "hex"); +const wrap = createCipheriv("aes192-wrap", kek, icv); +const wrapped = Buffer.concat([wrap.update(Buffer.alloc(16, 9)), wrap.final()]); +console.log(wrapped.toString("base64")); +const unwrap = createDecipheriv("id-aes192-wrap", kek, icv); +console.log(Buffer.concat([unwrap.update(wrapped), unwrap.final()]).toString("hex")); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 27e68a618..7c468de01 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -74,7 +74,7 @@ http2.connect("https://localhost"); * secure heap the generic hint, and setFips the FIPS truth (getFips() lowers to 0). */ import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; publicEncrypt("key", Buffer.alloc(1)); -createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); +createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index cabce49c4..562733f53 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -120,22 +120,22 @@ fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node bu 75 | import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; 76 | publicEncrypt("key", Buffer.alloc(1)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); hint: RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman -fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet +fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ocb'' is part of the standard library types but has no scriptc lowering yet 76 | publicEncrypt("key", Buffer.alloc(1)); - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ 78 | secureHeapUsed(); - hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 + hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 fenced.ts:78:1 - error SC2020: 'crypto.secureHeapUsed' is typed by @types/node but has no scriptc lowering yet - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); 78 | secureHeapUsed(); | ^~~~~~~~~~~~~~~~ 79 | setFips(false); diff --git a/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json b/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json new file mode 100644 index 000000000..4eec93aec --- /dev/null +++ b/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json @@ -0,0 +1 @@ +{ "plain": 6115, "san": 62881 } diff --git a/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json b/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json new file mode 100644 index 000000000..4968524bd --- /dev/null +++ b/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json @@ -0,0 +1 @@ +{ "plain": 8462, "san": 75851 } diff --git a/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json b/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json new file mode 100644 index 000000000..b5e45dae6 --- /dev/null +++ b/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json @@ -0,0 +1 @@ +{ "plain": 5548, "san": 52523 } diff --git a/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json b/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json new file mode 100644 index 000000000..425ec1dc2 --- /dev/null +++ b/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 3930, "san": 6119 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8b0b51b8..35583a188 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -829,7 +829,7 @@ describe("unimplemented cipher names", () => { const r = await compileAndRun( "cipher-computed-name", `import { createCipheriv } from "node:crypto"; -const mode = ["c", "c", "m"].join(""); +const mode = ["o", "c", "b"].join(""); try { createCipheriv("aes-128-" + mode, Buffer.alloc(16), Buffer.alloc(12)); } catch (error) { @@ -846,16 +846,43 @@ try { ); expect(r.exitCode).toBe(0); expect(r.stdout).toBe( - "Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " + - "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " + - "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + - "chacha20-poly1305\n" + + "Error SC2020 The cipher 'aes-128-ocb' has no native implementation; the supported ciphers are " + + "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their " + + "wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and " + + "ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and chacha20-poly1305\n" + "Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n", ); expect(r.stderr).toBe(""); }); }); +// getCipherInfo's result is Node's info object; CipherInfo declares nid as +// required, and Node reports the SIV ciphers without one. +describe("getCipherInfo through CipherInfo", () => { + test("a typed read of a missing nid throws a TypeError", async () => { + const r = await compileAndRun( + "cipher-info-no-nid", + `import { getCipherInfo } from "node:crypto"; +const siv = getCipherInfo("aes-128-siv")!; +console.log(JSON.stringify(siv), getCipherInfo("aes-128-gcm")?.nid); +try { + const nid: number = siv.nid; + console.log(nid); +} catch (error) { + const e = error as Error; + console.log(e.name, e.message); +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + '{"mode":"siv","name":"aes-128-siv","blockSize":1,"keyLength":32} 895\n' + + "TypeError expected number at $, got undefined\n", + ); + expect(r.stderr).toBe(""); + }); +}); + // new tls.TLSSocket reads a runtime options record: the client mode, whose // handshake Node never starts, and members without a lowering throw the // fence, as do sockets it cannot take over (a second wrap, and From 268194e18e070af82053ef0e4ce77ad2db22e9ab Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:43:28 -0700 Subject: [PATCH 047/169] Read records by keyof keys when their fields differ in type A keyed read of a record whose declared fields have different types, with a key the checker types as keyof the record (row[key] in a loop over Object.keys(patch) as (keyof Row)[]), was refused: the read needed one common field type. The checker already types such a read as the union of the field types; when every declared field surfaces in that union, the existing per-field dispatch wraps each hit into its arm. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/element-access.ts | 9 ++++ .../7400-record-keyof-union-read.ts.json | 6 +++ tests/corpus/7400-record-keyof-union-read.ts | 41 +++++++++++++++++++ .../7400-record-keyof-union-read.ts.json | 1 + 4 files changed, 57 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json create mode 100644 tests/corpus/7400-record-keyof-union-read.ts create mode 100644 tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 3ca1471ae..045bebdf4 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -1321,6 +1321,15 @@ export function lowerRecordKeyRead( shape.fields.every((f) => typeEquals(f.type, shape.fields[0]!.type)) ) { declared = shape.fields[0]!.type; + } else if (shape.fields.length > 0) { + // Fields of different types (`row[key]` with `key: keyof Row`): the + // checker types the read as the union of the field types the key can + // name. When every declared field surfaces in that union (an arm, or a + // union that widens into it), the per-field dispatch wraps each hit + // into its arm. + const read = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (read !== null && read.kind === "union" && recordKeyResultOk(lowerer, shape, read)) + declared = read; } // Fields of different types answer a checked-dynamic value where nothing // typed the read: in JavaScript, and where the checker types it 'any' diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json new file mode 100644 index 000000000..4f2a4c712 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7400-record-keyof-union-read.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7400-record-keyof-union-read.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7400-record-keyof-union-read.ts b/tests/corpus/7400-record-keyof-union-read.ts new file mode 100644 index 000000000..4a62a5067 --- /dev/null +++ b/tests/corpus/7400-record-keyof-union-read.ts @@ -0,0 +1,41 @@ +// Keyed reads of records whose fields have different types, with a key +// typed as `keyof` the record: the read is the union of the field types. +interface Book { + readonly id: string; + readonly title: string; + readonly year: number; + readonly tags: readonly string[]; + readonly available: boolean; +} +type BookPatch = Partial>; + +function changedKeys(current: Book, patch: BookPatch): string[] { + const changed: string[] = []; + for (const key of Object.keys(patch) as (keyof BookPatch)[]) { + const before = current[key]; + const after = patch[key]; + if (after === undefined) continue; + if (Array.isArray(before) && Array.isArray(after)) { + if (before.join("|") !== after.join("|")) changed.push(key); + } else if (before !== after) changed.push(key); + } + return changed; +} + +function describe(book: Book, key: keyof Book): string { + const value = book[key]; + if (typeof value === "string") return `${key}: string ${value}`; + if (typeof value === "number") return `${key}: number ${value.toFixed(1)}`; + if (typeof value === "boolean") return `${key}: boolean ${value ? "yes" : "no"}`; + return `${key}: list of ${value.length} [${value.join(", ")}]`; +} + +const book: Book = { id: "b1", title: "Atlas", year: 1999, tags: ["maps", "travel"], available: true }; +console.log(changedKeys(book, { title: "Atlas", year: 2001 })); +console.log(changedKeys(book, { tags: ["maps", "travel"], available: false })); +console.log(changedKeys(book, { tags: ["maps"], title: "Atlas II", year: undefined })); +for (const key of ["id", "title", "year", "tags", "available"] as const) console.log(describe(book, key)); +const keys: (keyof Book)[] = ["year", "tags"]; +const values = keys.map((k) => book[k]); +console.log(values, values.length); +console.log(JSON.stringify(Object.fromEntries(keys.map((k) => [k, book[k]])))); diff --git a/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json b/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json new file mode 100644 index 000000000..9b72eaf8d --- /dev/null +++ b/tests/harness/corpus-costs/7400-record-keyof-union-read.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20500 } From 05e0a19ff4e3c5424b73618592b3990e80add81e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:43:33 -0700 Subject: [PATCH 048/169] Spread untyped sources into typed object literals An object literal that spreads an `any` value into a typed slot (service.list({ ...c.req.valid("query"), offset: 0 }) where the validated value is untyped) was refused, because only known record shapes spread. TypeScript types such a literal `any` and the copy holds whatever own properties the source has at runtime, so it now builds as a checked-dynamic object and checks into the expected record like any other `any` value assigned to that slot. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/object-literals.ts | 16 ++++++++++ ...7401-any-spread-into-typed-literal.ts.json | 6 ++++ .../7401-any-spread-into-typed-literal.ts | 32 +++++++++++++++++++ ...7401-any-spread-into-typed-literal.ts.json | 1 + 4 files changed, 55 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json create mode 100644 tests/corpus/7401-any-spread-into-typed-literal.ts create mode 100644 tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 678b375fe..5f34280c8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -799,6 +799,22 @@ export function lowerObjectLiteral( expr: ts.ObjectLiteralExpression, expected?: IrType & { kind: "record" }, ): IrExpr { + // A spread of an untyped (`any`) source into a typed slot: TypeScript + // types such a literal `any`, and the copy holds whatever own properties + // the source has at runtime. Build it as a checked-dynamic object, then + // check it into the expected record like any other `any` value assigned + // to that slot. + if ( + expected && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && + expr.properties.nodes.some( + (prop) => + ts.isSpreadAssignment(prop) && + (lowerer.typeOf(prop.expression).flags & ts.TypeFlags.Any) !== 0, + ) + ) { + return lowerer.coerceToExpected(lowerDynObjectLiteral(lowerer, expr), expected); + } // Unannotated JavaScript accumulator fields have no element contract. // Preserve their checked arrays instead of assigning never[] a numeric ABI. // An empty object field is a map the program adds keys to. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json new file mode 100644 index 000000000..3e3749b3f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-any-spread-into-typed-literal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7401-any-spread-into-typed-literal.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7401-any-spread-into-typed-literal.ts b/tests/corpus/7401-any-spread-into-typed-literal.ts new file mode 100644 index 000000000..c159a813c --- /dev/null +++ b/tests/corpus/7401-any-spread-into-typed-literal.ts @@ -0,0 +1,32 @@ +// An object literal spreading an untyped (`any`) source into a typed slot +// copies the source's runtime properties, then checks the copy into the +// slot's type like any other `any` value. +interface Filter { + readonly genre?: string; + readonly sort: string; + readonly offset: number; + readonly limit: number; +} + +function untyped(text: string): any { + return JSON.parse(text); +} + +function describe(filter: Filter): string { + return `${filter.genre ?? "(any genre)"} sorted by ${filter.sort}, ${filter.offset}..${filter.offset + filter.limit}`; +} + +function main(): void { + const query = untyped('{"genre":"poetry","sort":"title","offset":20,"limit":10}'); + console.log(describe({ ...query, offset: 0, limit: 100 })); + console.log(describe({ offset: 5, ...query })); + const merged: Filter = { sort: "added", offset: 0, limit: 20, ...untyped('{"limit":3}') }; + console.log(merged.sort, merged.offset, merged.limit, merged.genre === undefined); + const copy: { a: number; b: string } = { ...untyped('{"a":1,"b":"x"}'), b: "y" }; + console.log(copy.a + 1, copy.b, JSON.stringify(copy)); + let latest: { a: number } = { a: 0 }; + latest = { ...untyped('{"a":4}') }; + console.log(latest.a); +} + +main(); diff --git a/tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json b/tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json new file mode 100644 index 000000000..9eb118813 --- /dev/null +++ b/tests/harness/corpus-costs/7401-any-spread-into-typed-literal.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20000 } From 8bc8fc54ca42e5e4abea07afea8ff7b0a495e423 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:47:26 -0500 Subject: [PATCH 049/169] Bound string reads of while loops by their counter tests With while-loop versions, `while (pos < end && s.charCodeAt(pos) === 32) pos++` took a version whose guard could never hold. The counter scan records only typed-array accesses, so the string read fell back to the statically ranged check. For a counter held in an int32 slot, that check asks for a length above 2^31 - 2. The loop paid for the guard and ran its checked copy. The counter scan now records `s.charCodeAt(i)` and `s.charAt(i)` of a string like typed-array reads. Their guard compares the counter's bound with the string's length, as for a typed array. Corpus 7302 adds while loops over counters below a string's length. --- .../backend/llvm/bytes-loop-versions.test.ts | 45 +++++++++++ .../src/backend/llvm/bytes-loop-versions.ts | 29 ++++--- .../corpus/7302-string-scan-loop-versions.ts | 76 ++++++++++++++++++- 3 files changed, 137 insertions(+), 13 deletions(-) diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index f356b9849..805715386 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -557,3 +557,48 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => // The checked copy keeps the inline helper and its fallback. expect(body).toContain("@sc_str_char_code_at_int"); }); + +test("while loop counters below a string's length version its reads", () => { + // while (i < text.length && text.charCodeAt(i) === 32) i = i + 1 + const text = ref("text", STR); + const read = charCode(text, ref("i")); + const length: IrExpr = { + kind: "strIntrinsic", + method: "length", + receiver: text, + args: [], + type: F64, + loc, + }; + const loop: IrStmt & { kind: "while" } = { + kind: "while", + cond: { + kind: "logical", + op: "&&", + left: { kind: "bin", op: "<", left: ref("i"), right: length, type: BOOL, loc }, + right: { kind: "bin", op: "===", left: read, right: num(32), type: BOOL, loc }, + type: BOOL, + loc, + }, + body: [{ kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }], + loc, + }; + const f = textKernel([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); + const version = matchBytesLoopVersion(loop, null, { + locals: new Map(f.locals.map((l) => [l.id, l])), + invariantGlobals: new Set(), + captured: new Set(), + ranges: analyzeIntegerRanges(f), + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + scalarStrings: new Set(), + workers: false, + owned: new Set(), + })!; + // The read is bounded by the counter's test against the length, not by + // a static range the guard could not meet. + expect(version.bounds.has(read)).toBe(true); + expect(version.receivers.map((r) => r.string)).toEqual([true]); + expect(version.bounded.map((c) => c.slack)).toEqual([0]); + expect(version.fixed).toEqual([]); +}); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index c73344baf..35937328f 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -711,17 +711,18 @@ function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | nul return { id: s.localId, k: value.right.value, wraps }; } -/** Typed-array accesses of a while loop that happen at a counter below a - * loop-invariant bound. A counter is a number local that the loop changes - * only by positive integer increments, written as statements of the body, - * of its blocks and branches, or of nested while loops. The condition of - * the loop, an `if` or a `&&` chain that compares the counter below a bound - * (`while (pos < end && bytes[pos] === 32) pos++`) makes it below that - * bound until the next increment on the path. The counter starts at its - * entry value and only grows (a `| 0` increment never wraps when every - * bound is at most 2^31 minus the increments), so an access at `counter + - * d` is in bounds for the whole loop when the entry value is at least `-d` - * and `bound + d < length` (`+ 1` for `<=`). */ +/** Typed-array and string accesses of a while loop that happen at a + * counter below a loop-invariant bound. A counter is a number local that + * the loop changes only by positive integer increments, written as + * statements of the body, of its blocks and branches, or of nested while + * loops. The condition of the loop, an `if` or a `&&` chain that compares + * the counter below a bound (`while (pos < end && bytes[pos] === 32) + * pos++`, or `s.charCodeAt(pos)` of a string) makes it below that bound + * until the next increment on the path. The counter starts at its entry + * value and only grows (a `| 0` increment never wraps when every bound is + * at most 2^31 minus the increments), so an access at `counter + d` is in + * bounds for the whole loop when the entry value is at least `-d` and + * `bound + d < length` (`+ 1` for `<=`). */ function findWhileCounterAccesses( loop: IrStmt & { kind: "while" }, host: { @@ -831,6 +832,12 @@ function findWhileCounterAccesses( : undefined; if (index) record(e, index, clean); } + if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] + ) + record(e, e.args[0], clean); if (e.kind === "logical" && e.op === "&&") { expr(e.left, clean); expr(e.right, withFacts(clean, e.left)); diff --git a/tests/corpus/7302-string-scan-loop-versions.ts b/tests/corpus/7302-string-scan-loop-versions.ts index a8a975d86..3408e048e 100644 --- a/tests/corpus/7302-string-scan-loop-versions.ts +++ b/tests/corpus/7302-string-scan-loop-versions.ts @@ -2,8 +2,10 @@ // text, surrogate pairs and lone surrogates, empty strings, neighbors at // i - 1 and i + 1, loops that stop early or read past the end, strings built // by concatenation and slicing, a scan of a slice local, two strings read at -// one index, a string rebound inside or after the loop, nested scans and a -// module-level scan. +// one index, a string rebound inside or after the loop, nested scans, a +// module-level scan, and while loops over a counter below the length: word +// and identifier scans, peeks past the counter, a counter from a field, and +// reads at or past the end. function spaces(s: string): number { let n = 0; @@ -75,6 +77,75 @@ function rebound(s: string): number { return n; } +function words(s: string): number { + let pos = 0, + n = 0; + const end = s.length; + while (pos < end) { + while (pos < end && s.charCodeAt(pos) === 32) pos++; + if (pos >= end) break; + n++; + while (pos < end && s.charCodeAt(pos) !== 32) pos++; + } + return n; +} + +function identifiers(s: string): number { + let pos = 0, + total = 0; + while (pos < s.length) { + const c = s.charCodeAt(pos); + if (c >= 97 && c <= 122) { + const start = pos; + while (pos < s.length && s.charCodeAt(pos) >= 97 && s.charCodeAt(pos) <= 122) pos++; + total += pos - start; + } else pos = (pos + 1) | 0; + } + return total; +} + +function pairs(s: string): number { + let pos = 0, + n = 0; + while (pos < s.length) { + if (s.charCodeAt(pos) === s.charCodeAt(pos + 1)) n++; + pos += 1; + } + return n; +} + +function throughEnd(s: string): string { + let pos = 0, + sum = 0; + while (pos <= s.length) { + sum += s.charCodeAt(pos); + pos++; + } + return String(sum); +} + +class Cursor { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + skip(): number { + const src = this.src; + let pos = this.pos; + while (pos < src.length && src.charCodeAt(pos) !== 44) pos++; + this.pos = pos + 1; + return pos; + } +} + +function fields(s: string): string { + const cursor = new Cursor(s); + const ends: number[] = []; + while (cursor.pos < s.length) ends.push(cursor.skip()); + return ends.join(","); +} + const samples = [ "the quick brown fox jumps over the lazy dog", "", @@ -99,6 +170,7 @@ for (const s of samples) { rebound(s), slicedScan(s, 2, 9), ); + console.log(" ", words(s), identifiers(s), pairs(s), throughEnd(s), fields(s)); } let built = ""; From a4287e91229f0c6fdf3825a2781ebb7d275c6229 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:48:23 -0700 Subject: [PATCH 050/169] Keep Linux poller timers in a heap behind one timerfd The epoll poller gave every armed timer its own timerfd. A keep-alive HTTP server re-arms and cancels the socket idle timer on every request, which cost six system calls per request (timerfd_create, timerfd_settime twice, epoll_ctl twice, close) on top of the read and the send; Node makes two. Armed keys now live in a deadline min-heap with a pointer hash, and one timerfd wakes the loop at the earliest deadline. Arming a later deadline, re-arming and cancelling make no system call; the timerfd is reprogrammed only when an earlier deadline arrives or when it fires. The fd table is indexed by fd number instead of scanned. Delivery is unchanged: each expired timer reports once (one-shot), now in deadline order. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_loop_epoll.c | 296 +++++++++++++++----- packages/runtime/src/scr_loop_epoll.test.c | 108 +++++++ packages/runtime/src/scr_loop_epoll.test.ts | 41 +++ 3 files changed, 381 insertions(+), 64 deletions(-) create mode 100644 packages/runtime/src/scr_loop_epoll.test.c create mode 100644 packages/runtime/src/scr_loop_epoll.test.ts diff --git a/packages/runtime/src/scr_loop_epoll.c b/packages/runtime/src/scr_loop_epoll.c index 484ef36b6..8040c76fe 100644 --- a/packages/runtime/src/scr_loop_epoll.c +++ b/packages/runtime/src/scr_loop_epoll.c @@ -1,6 +1,6 @@ /* The Linux backend of the platform readiness contract (scr_platform.h): - * epoll for fd readiness, one timerfd per armed one-shot timer. This is - * the LIVE Linux poller behind scr_net.c and scr_dgram.c (native-toolchain.ts links it + * epoll for fd readiness, one timerfd for every armed one-shot timer. This + * is the LIVE Linux poller behind scr_net.c and scr_dgram.c (native-toolchain.ts links it * alongside scr_loop_kqueue.c whenever those units compile; each TU is * empty off its platform). Child-exit wakeups are NOT here: scr_child.c * keeps its narrow seam inline (kqueue EVFILT_PROC on BSD, pidfd_open + @@ -9,18 +9,22 @@ * * Design notes: * - Interest is keyed by fd with ONE mask under epoll, so the poller keeps - * a linear fd -> (mask, udata) table and merges read/write interest into - * EPOLL_CTL_MOD. Linear scan is fine at the units' scale (dozens of fds, - * drained in 64-event batches today); swap for a hash if profiles say so. + * an fd -> (mask, udata) table (indexed by fd number) and merges + * read/write interest into EPOLL_CTL_MOD. * - EPOLLHUP/EPOLLERR/EPOLLRDHUP report as READABLE (plus WRITABLE when * write interest is armed): the units learn EOF/errors from read()==0 or * errno exactly as they do from kqueue's EV_EOF today. Note an fd is in * the epoll set ONLY while some interest is armed, so a consumer-less * socket can never busy-report HUP. - * - Timers: EVFILT_TIMER has no epoll analogue, so each armed key owns a - * TFD_NONBLOCK timerfd registered EPOLLIN in the same epoll set, tagged - * through the same table (mask=TIMER). Fired timers are disarmed and - * their timerfd closed at delivery (kqueue's EV_ONESHOT semantics). + * - Timers: EVFILT_TIMER has no epoll analogue. Armed keys live in a + * deadline min-heap (a pointer hash finds a key's slot), and ONE timerfd + * in the epoll set wakes the loop at the earliest deadline. Re-arming, + * arming a later deadline and cancelling make no system call: the + * timerfd is reprogrammed only when a deadline earlier than its own + * arrives, or when it fires early (the heap then holds later deadlines). + * A server's per-request idle-timer churn therefore costs nothing in the + * kernel, like libuv's timer heap. Expired timers are delivered (and + * disarmed: kqueue's EV_ONESHOT semantics) by the next drain. * - Units must scrp_forget a watched fd before close(2) (the contract's * epoll obligation). As a second line of defense against a missed * forget followed by fd-number reuse, watch self-heals: a MOD that @@ -32,47 +36,75 @@ #include "scr_platform.h" #include +#include #include #include #include #include #include +#include #include /* ── fd/timer poller ─────────────────────────────────────────────────── */ typedef struct { - int fd; /* watched fd, or the timer's timerfd */ - unsigned mask; /* SCRP_READABLE|SCRP_WRITABLE, or SCRP_TIMER */ - void *udata; /* delivery tag; for timers: the arm-time udata */ - void *timer_key; /* timers only: the caller's key */ + int fd; /* watched fd */ + unsigned mask; /* SCRP_READABLE|SCRP_WRITABLE */ + void *udata; /* delivery tag */ } ScrpEntry; +typedef struct { + double deadline; /* CLOCK_MONOTONIC milliseconds */ + uint64_t seq; /* arm order: equal deadlines fire first-armed first */ + void *key; + void *udata; +} ScrpTimer; + +typedef struct { + void *key; + size_t idx; /* the key's heap index */ +} ScrpKeySlot; + struct ScrPoller { int ep; ScrpEntry *entries; size_t n, cap; + size_t *slot_of_fd; /* fd -> entry index + 1 (0: unwatched) */ + size_t fd_cap; + int tfd; /* the shared timerfd (-1 until a timer arms) */ + double tfd_deadline; /* what tfd is programmed for; INFINITY: disarmed */ + ScrpTimer *heap; + size_t nt, heap_cap; + ScrpKeySlot *key_slots; /* open addressing over keys (key NULL: empty) */ + size_t key_cap; /* power of two */ + uint64_t seq; }; static ScrpEntry *scrp_find_fd(ScrPoller *p, int fd) { - for (size_t i = 0; i < p->n; i++) - if (p->entries[i].fd == fd) return &p->entries[i]; - return NULL; -} - -static ScrpEntry *scrp_find_key(ScrPoller *p, void *key) { - for (size_t i = 0; i < p->n; i++) - if ((p->entries[i].mask & SCRP_TIMER) != 0 && p->entries[i].timer_key == key) return &p->entries[i]; - return NULL; + if (fd < 0 || (size_t)fd >= p->fd_cap || p->slot_of_fd[fd] == 0) return NULL; + return &p->entries[p->slot_of_fd[fd] - 1]; } static void scrp_remove(ScrPoller *p, ScrpEntry *e) { size_t i = (size_t)(e - p->entries); - p->entries[i] = p->entries[p->n - 1]; + p->slot_of_fd[e->fd] = 0; p->n--; + if (i != p->n) { + p->entries[i] = p->entries[p->n]; + p->slot_of_fd[p->entries[i].fd] = i + 1; + } } static bool scrp_push(ScrPoller *p, ScrpEntry e) { + if ((size_t)e.fd >= p->fd_cap) { + size_t cap = p->fd_cap == 0 ? 64 : p->fd_cap; + while (cap <= (size_t)e.fd) cap *= 2; + size_t *grown = realloc(p->slot_of_fd, cap * sizeof *grown); + if (grown == NULL) return false; + memset(grown + p->fd_cap, 0, (cap - p->fd_cap) * sizeof *grown); + p->slot_of_fd = grown; + p->fd_cap = cap; + } if (p->n == p->cap) { size_t cap = p->cap == 0 ? 16 : p->cap * 2; ScrpEntry *grown = realloc(p->entries, cap * sizeof *grown); @@ -81,6 +113,117 @@ static bool scrp_push(ScrPoller *p, ScrpEntry e) { p->cap = cap; } p->entries[p->n++] = e; + p->slot_of_fd[e.fd] = p->n; + return true; +} + +/* ── the timer heap ── */ + +static double scrp_now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; +} + +static size_t scrp_key_hash(const ScrPoller *p, const void *key) { + uintptr_t h = (uintptr_t)key; + h ^= h >> 17; + h *= (uintptr_t)0x9E3779B97F4A7C15ull; + h ^= h >> 29; + return (size_t)h & (p->key_cap - 1); +} + +/* The hash slot holding `key`, or the empty slot where it would go. */ +static size_t scrp_key_probe(const ScrPoller *p, const void *key) { + size_t i = scrp_key_hash(p, key); + while (p->key_slots[i].key != NULL && p->key_slots[i].key != key) + i = (i + 1) & (p->key_cap - 1); + return i; +} + +static ScrpKeySlot *scrp_key_find(const ScrPoller *p, const void *key) { + if (p->key_cap == 0) return NULL; + size_t i = scrp_key_probe(p, key); + return p->key_slots[i].key != NULL ? &p->key_slots[i] : NULL; +} + +/* Backward-shift deletion keeps linear probing tombstone-free. */ +static void scrp_key_erase(ScrPoller *p, size_t i) { + size_t mask = p->key_cap - 1; + p->key_slots[i].key = NULL; + for (size_t j = (i + 1) & mask; p->key_slots[j].key != NULL; j = (j + 1) & mask) { + size_t home = scrp_key_hash(p, p->key_slots[j].key); + /* move j into the hole when its home is not cyclically within (i, j] */ + if (((j - home) & mask) >= ((j - i) & mask)) { + p->key_slots[i] = p->key_slots[j]; + p->key_slots[j].key = NULL; + i = j; + } + } +} + +static bool scrp_key_grow(ScrPoller *p) { + size_t cap = p->key_cap == 0 ? 64 : p->key_cap * 2; + ScrpKeySlot *slots = calloc(cap, sizeof *slots); + if (slots == NULL) return false; + free(p->key_slots); + p->key_slots = slots; + p->key_cap = cap; + for (size_t h = 0; h < p->nt; h++) + p->key_slots[scrp_key_probe(p, p->heap[h].key)] = (ScrpKeySlot){p->heap[h].key, h}; + return true; +} + +static bool scrp_timer_less(const ScrpTimer *a, const ScrpTimer *b) { + return a->deadline < b->deadline || (a->deadline == b->deadline && a->seq < b->seq); +} + +/* Place `t` at heap index `i` and record it in the key table. */ +static void scrp_heap_put(ScrPoller *p, size_t i, ScrpTimer t) { + p->heap[i] = t; + scrp_key_find(p, t.key)->idx = i; +} + +static void scrp_heap_fix(ScrPoller *p, size_t i) { + ScrpTimer t = p->heap[i]; + while (i > 0) { + size_t parent = (i - 1) / 2; + if (!scrp_timer_less(&t, &p->heap[parent])) break; + scrp_heap_put(p, i, p->heap[parent]); + i = parent; + } + for (;;) { + size_t child = 2 * i + 1; + if (child >= p->nt) break; + if (child + 1 < p->nt && scrp_timer_less(&p->heap[child + 1], &p->heap[child])) child++; + if (!scrp_timer_less(&p->heap[child], &t)) break; + scrp_heap_put(p, i, p->heap[child]); + i = child; + } + scrp_heap_put(p, i, t); +} + +static void scrp_heap_remove(ScrPoller *p, size_t i) { + scrp_key_erase(p, scrp_key_probe(p, p->heap[i].key)); + p->nt--; + if (i == p->nt) return; + p->heap[i] = p->heap[p->nt]; + scrp_heap_fix(p, i); +} + +/* Program the shared timerfd for `deadline` (INFINITY disarms). */ +static bool scrp_tfd_program(ScrPoller *p, double deadline) { + struct itimerspec its; + memset(&its, 0, sizeof its); + if (deadline != INFINITY) { + if (deadline < 0) deadline = 0; + its.it_value.tv_sec = (time_t)(deadline / 1000.0); + its.it_value.tv_nsec = (long)((deadline - (double)its.it_value.tv_sec * 1000.0) * 1e6); + if (its.it_value.tv_nsec >= 1000000000L) its.it_value.tv_nsec = 999999999L; + if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1; /* 0 disarms */ + } + if (timerfd_settime(p->tfd, TFD_TIMER_ABSTIME, &its, NULL) != 0) return false; + p->tfd_deadline = deadline; return true; } @@ -92,18 +235,21 @@ ScrPoller *scrp_poller_new(void) { free(p); return NULL; } + p->tfd = -1; + p->tfd_deadline = INFINITY; return p; } void scrp_poller_free(ScrPoller *p) { if (p == NULL) return; - for (size_t i = 0; i < p->n; i++) - if ((p->entries[i].mask & SCRP_TIMER) != 0) close(p->entries[i].fd); + if (p->tfd >= 0) close(p->tfd); close(p->ep); free(p->entries); + free(p->slot_of_fd); + free(p->heap); + free(p->key_slots); free(p); } - int scrp_poller_fd(const ScrPoller *p) { return p->ep; } static uint32_t scrp_epoll_mask(unsigned mask) { @@ -124,7 +270,7 @@ static bool scrp_watch(ScrPoller *p, int fd, void *udata, bool on, unsigned bit) struct epoll_event ev = {.events = scrp_epoll_mask(next), .data = {.fd = fd}}; if (e == NULL) { if (next == 0) return true; /* unwatch of an unwatched fd: no-op */ - if (!scrp_push(p, (ScrpEntry){fd, next, udata, NULL})) return false; + if (!scrp_push(p, (ScrpEntry){fd, next, udata})) return false; int rc = epoll_ctl(p->ep, EPOLL_CTL_ADD, fd, &ev); if (rc != 0 && errno == EEXIST) rc = epoll_ctl(p->ep, EPOLL_CTL_MOD, fd, &ev); if (rc != 0) { @@ -163,41 +309,51 @@ void scrp_forget(ScrPoller *p, int fd) { bool scrp_timer_arm(ScrPoller *p, void *key, double ms, void *udata) { if (!(ms >= 0)) ms = 0; - ScrpEntry *e = scrp_find_key(p, key); - int tfd = e != NULL ? e->fd : timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); - if (tfd < 0) return false; - struct itimerspec its; - memset(&its, 0, sizeof its); - its.it_value.tv_sec = (time_t)(ms / 1000.0); - its.it_value.tv_nsec = (long)((ms - (double)its.it_value.tv_sec * 1000.0) * 1e6); - if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1; /* 0 disarms timerfd */ - if (timerfd_settime(tfd, 0, &its, NULL) != 0) { - if (e == NULL) close(tfd); - return false; + if (p->tfd < 0) { + int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (tfd < 0) return false; + struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = tfd}}; + if (epoll_ctl(p->ep, EPOLL_CTL_ADD, tfd, &ev) != 0) { + close(tfd); + return false; + } + p->tfd = tfd; } - if (e != NULL) { - e->udata = udata; - return true; /* re-arm replaces the deadline */ + ScrpTimer t = {scrp_now_ms() + ms, p->seq++, key, udata}; + ScrpKeySlot *slot = scrp_key_find(p, key); + if (slot != NULL) { + /* re-arm replaces the deadline */ + size_t i = slot->idx; + p->heap[i] = t; + scrp_heap_fix(p, i); + } else { + if (p->nt == p->heap_cap) { + size_t cap = p->heap_cap == 0 ? 16 : p->heap_cap * 2; + ScrpTimer *grown = realloc(p->heap, cap * sizeof *grown); + if (grown == NULL) return false; + p->heap = grown; + p->heap_cap = cap; + } + if ((p->nt + 1) * 2 > p->key_cap && !scrp_key_grow(p)) return false; + p->heap[p->nt] = t; + p->key_slots[scrp_key_probe(p, key)] = (ScrpKeySlot){key, p->nt}; + p->nt++; + scrp_heap_fix(p, p->nt - 1); } - if (!scrp_push(p, (ScrpEntry){tfd, SCRP_TIMER, udata, key})) { - close(tfd); - return false; - } - struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = tfd}}; - if (epoll_ctl(p->ep, EPOLL_CTL_ADD, tfd, &ev) != 0) { - scrp_remove(p, &p->entries[p->n - 1]); - close(tfd); + /* Only an earlier deadline needs the kernel: a later one is found when + * the timerfd fires early. */ + if (p->heap[0].deadline < p->tfd_deadline && !scrp_tfd_program(p, p->heap[0].deadline)) { + scrp_heap_remove(p, scrp_key_find(p, key)->idx); return false; } return true; } void scrp_timer_cancel(ScrPoller *p, void *key) { - ScrpEntry *e = scrp_find_key(p, key); - if (e == NULL) return; /* unarmed or already fired: no-op */ - (void)epoll_ctl(p->ep, EPOLL_CTL_DEL, e->fd, NULL); - close(e->fd); - scrp_remove(p, e); + ScrpKeySlot *slot = scrp_key_find(p, key); + if (slot == NULL) return; /* unarmed or already fired: no-op */ + /* the timerfd keeps its deadline; an early wake finds nothing due */ + scrp_heap_remove(p, slot->idx); } int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) { @@ -205,20 +361,17 @@ int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) { struct epoll_event evs[64]; int want = max < 64 ? max : 64; int n = epoll_wait(p->ep, evs, want, 0); - if (n <= 0) return 0; /* EINTR/none: a spurious pass */ int filled = 0; + bool timer_woke = false; for (int i = 0; i < n; i++) { - ScrpEntry *e = scrp_find_fd(p, evs[i].data.fd); - if (e == NULL) continue; /* raced with forget */ - if ((e->mask & SCRP_TIMER) != 0) { - out[filled].udata = e->udata; - out[filled++].events = SCRP_TIMER; - /* one-shot: disarm at delivery, EV_ONESHOT semantics */ - (void)epoll_ctl(p->ep, EPOLL_CTL_DEL, e->fd, NULL); - close(e->fd); - scrp_remove(p, e); + if (evs[i].data.fd == p->tfd && p->tfd >= 0) { + uint64_t ticks; + (void)!read(p->tfd, &ticks, sizeof ticks); + timer_woke = true; continue; } + ScrpEntry *e = scrp_find_fd(p, evs[i].data.fd); + if (e == NULL) continue; /* raced with forget */ unsigned got = 0; uint32_t ep = evs[i].events; if ((ep & (EPOLLIN | EPOLLHUP | EPOLLERR | EPOLLRDHUP)) != 0) got |= SCRP_READABLE; @@ -229,6 +382,21 @@ int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) { out[filled].udata = e->udata; out[filled++].events = got; } + /* The timerfd always holds a deadline no later than the heap's earliest + * (arming an earlier one reprograms it), so a timer can only be due + * after it fires. */ + if (timer_woke) { + p->tfd_deadline = INFINITY; /* one-shot: it fired */ + /* deliver every expired timer the batch has room for, one-shot */ + double now = scrp_now_ms(); + while (p->nt > 0 && filled < max && p->heap[0].deadline <= now) { + out[filled].udata = p->heap[0].udata; + out[filled++].events = SCRP_TIMER; + scrp_heap_remove(p, 0); + } + /* the next deadline (one already past fires at once) */ + if (p->nt > 0) (void)scrp_tfd_program(p, p->heap[0].deadline); + } return filled; } diff --git a/packages/runtime/src/scr_loop_epoll.test.c b/packages/runtime/src/scr_loop_epoll.test.c new file mode 100644 index 000000000..4c0798e29 --- /dev/null +++ b/packages/runtime/src/scr_loop_epoll.test.c @@ -0,0 +1,108 @@ +#undef NDEBUG +#define _GNU_SOURCE 1 +#include "scr_platform.h" +#include +#include +#include +#include +#include +#include +#include + +/* The epoll poller alone: timers live in a deadline heap behind one + * timerfd, so arming, re-arming and cancelling many keys must still + * deliver each armed key exactly once, in deadline order, after its + * deadline, and never a cancelled key. */ + +enum { KEYS = 2000 }; + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; +} + +static int keys[KEYS]; +static double deadline[KEYS]; +static int state[KEYS]; /* 0 unarmed/cancelled, 1 armed, 2 fired */ + +/* Wait for the poller fd, then drain; returns the events drained. */ +static int pump(ScrPoller *p, ScrPollerEvent *evs, int max, int wait_ms) { + struct pollfd pfd = {.fd = scrp_poller_fd(p), .events = POLLIN}; + (void)poll(&pfd, 1, wait_ms); + return scrp_drain(p, evs, max); +} + +int main(void) { + ScrPoller *p = scrp_poller_new(); + assert(p != NULL); + srand(7); + double start = now_ms(); + for (int i = 0; i < KEYS; i++) { + double ms = 5 + rand() % 120; + assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); + deadline[i] = start + ms; + state[i] = 1; + } + /* re-arm a third later and earlier, cancel a third (the heap and the key + * table move entries on every one of these) */ + for (int i = 0; i < KEYS; i++) { + if (i % 3 == 0) { + double ms = 1 + rand() % 150; + double t = now_ms(); + assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); + deadline[i] = t + ms; + } else if (i % 3 == 1) { + scrp_timer_cancel(p, &keys[i]); + state[i] = 0; + } + } + scrp_timer_cancel(p, &keys[1]); /* cancel of a cancelled key: no-op */ + /* an fd delivers alongside the timers */ + int fds[2]; + assert(pipe(fds) == 0); + static int pipe_tag; + assert(scrp_watch_read(p, fds[0], &pipe_tag, true)); + assert(write(fds[1], "x", 1) == 1); + int armed = 0; + for (int i = 0; i < KEYS; i++) armed += state[i] == 1; + int fired = 0, piped = 0; + double last = 0; + ScrPollerEvent evs[16]; /* small batches leave due timers for the next drain */ + while (fired < armed) { + int n = pump(p, evs, 16, 1000); + for (int e = 0; e < n; e++) { + if (evs[e].udata == &pipe_tag) { + assert(evs[e].events == SCRP_READABLE); + char c; + assert(read(fds[0], &c, 1) == 1); + piped++; + continue; + } + assert(evs[e].events == SCRP_TIMER); + int i = (int)((int *)evs[e].udata - keys); + assert(i >= 0 && i < KEYS); + assert(state[i] == 1); + double t = now_ms(); + assert(t + 1.0 >= deadline[i]); /* never early (timerfd rounding aside) */ + assert(deadline[i] + 1.0 >= last); /* deadline order */ + if (deadline[i] > last) last = deadline[i]; + state[i] = 2; + fired++; + } + assert(now_ms() - start < 10000); + } + assert(piped == 1); + /* nothing else fires: the cancelled keys stay quiet */ + assert(pump(p, evs, 16, 200) == 0); + /* a key fires again after re-arming once it fired; 0 ms fires at once */ + assert(scrp_timer_arm(p, &keys[0], 0, &keys[0])); + int n = pump(p, evs, 16, 1000); + assert(n == 1 && evs[0].udata == &keys[0] && evs[0].events == SCRP_TIMER); + scrp_forget(p, fds[0]); + close(fds[0]); + close(fds[1]); + scrp_poller_free(p); + puts("epoll timers fire once each, in deadline order, never when cancelled"); + return 0; +} diff --git a/packages/runtime/src/scr_loop_epoll.test.ts b/packages/runtime/src/scr_loop_epoll.test.ts new file mode 100644 index 000000000..89b2e9c3d --- /dev/null +++ b/packages/runtime/src/scr_loop_epoll.test.ts @@ -0,0 +1,41 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const exec = promisify(execFile); + +/* The Linux poller links alone (it needs only libc), under AddressSanitizer + * and UndefinedBehaviorSanitizer. */ +test.skipIf(process.platform !== "linux")( + "the epoll poller's timer heap fires each armed key once, in deadline order", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-epoll-")); + try { + const bin = join(dir, "epoll"); + await exec("clang", [ + "-std=c11", + "-O1", + "-g", + "-fsanitize=address,undefined", + join(import.meta.dirname, "scr_loop_epoll.test.c"), + join(import.meta.dirname, "scr_loop_epoll.c"), + "-lm", + "-o", + bin, + ]); + const result = await exec(bin, [], { + timeout: 60000, + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(result.stdout).toBe( + "epoll timers fire once each, in deadline order, never when cancelled\n", + ); + expect(result.stderr).toBe(""); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); From 5372c69459cf4ce27f2d121029b8d451f99a43fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:50:29 -0500 Subject: [PATCH 051/169] Pass { signal } to stream operator callbacks and Duplex.from bodies --- .../builtin-apis/streams-and-readline.mdx | 7 +- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 4 +- .../compiler/src/backend/llvm/lib-shared.ts | 14 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../lowering/class-dynamic-dispatch.ts | 67 +- .../src/frontend/lowering/lower-stream.ts | 331 ++++++-- packages/compiler/src/ir/builtin-effects.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 43 +- packages/compiler/src/ir/runtime-features.ts | 4 + packages/compiler/src/ir/validate.ts | 27 +- .../test/native-codegen-integration.test.ts | 4 +- .../7433-readable-operator-signal.mjs.json | 6 + .../7434-readable-operator-values.ts.json | 6 + .../7435-readable-flatmap-iterables.mjs.json | 6 + .../7436-readable-read-any-value.mjs.json | 6 + .../corpus/7437-duplex-from-signal.mjs.json | 6 + .../corpus/7438-duplex-from-signal.ts.json | 6 + .../7439-readable-read-string-chunks.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_stream.h | 47 +- packages/runtime/src/scr_stream_ops.c | 711 +++++++++++++++--- .../corpus/7433-readable-operator-signal.mjs | 36 + tests/corpus/7434-readable-operator-values.ts | 32 + .../7435-readable-flatmap-iterables.mjs | 29 + tests/corpus/7436-readable-read-any-value.mjs | 17 + tests/corpus/7437-duplex-from-signal.mjs | 54 ++ tests/corpus/7438-duplex-from-signal.ts | 26 + .../7439-readable-read-string-chunks.ts | 17 + .../7433-readable-operator-signal.mjs.json | 1 + .../7434-readable-operator-values.ts.json | 1 + .../7435-readable-flatmap-iterables.mjs.json | 1 + .../7436-readable-read-any-value.mjs.json | 1 + .../7437-duplex-from-signal.mjs.json | 1 + .../7438-duplex-from-signal.ts.json | 1 + .../7439-readable-read-string-chunks.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- tests/harness/stream-entry-kinds.test.ts | 77 +- 43 files changed, 1321 insertions(+), 301 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json create mode 100644 tests/corpus/7433-readable-operator-signal.mjs create mode 100644 tests/corpus/7434-readable-operator-values.ts create mode 100644 tests/corpus/7435-readable-flatmap-iterables.mjs create mode 100644 tests/corpus/7436-readable-read-any-value.mjs create mode 100644 tests/corpus/7437-duplex-from-signal.mjs create mode 100644 tests/corpus/7438-duplex-from-signal.ts create mode 100644 tests/corpus/7439-readable-read-string-chunks.ts create mode 100644 tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json create mode 100644 tests/harness/corpus-costs/7434-readable-operator-values.ts.json create mode 100644 tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json create mode 100644 tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json create mode 100644 tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json create mode 100644 tests/harness/corpus-costs/7438-duplex-from-signal.ts.json create mode 100644 tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index aa0afd352..fe6bb2dfb 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -10,7 +10,7 @@ The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built. In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there. An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value. - `readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`). + `readable.read()` on an object-mode stream answers any value it holds. `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error. `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument. A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. @@ -20,10 +20,11 @@ `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`). In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error. `Readable.from` accepts `objectMode` and `highWaterMark` options. - The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. + The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options. + Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream. `Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable. It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`. - A web stream and a body function that declares the options argument are unsupported in `Duplex.from`. + A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with. `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`. A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 5b917c63b..70028ab2d 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, + runtime_abi: { version: 32, marker: "scr_runtime_abi_v32" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v31"); + expect(error?.stderr).toContain("scr_runtime_abi_v32"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index bd8f78105..c2244a233 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -7272,7 +7272,9 @@ declare module "stream" { push(chunk: any, encoding?: string): boolean; unshift(chunk: Buffer | string, encoding?: string): void; unshift(chunk: any, encoding?: string): void; - read(size?: number): Buffer | null; + /* A Buffer, a string once encoded, an object-mode value, or null (as + * @types/node types it). */ + read(size?: number): any; pause(): this; resume(): this; isPaused(): boolean; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e433832a..cacff945d 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1134,6 +1134,8 @@ export const LIB_FN_SYMS: Record = { "stream.duplexifyPair": "scr_stream_duplexify_pair", "stream.duplexFromPromise": "scr_stream_duplex_from_promise", "stream.duplexFromAsyncFn": "scr_stream_duplex_from_async_fn", + "stream.duplexFromGenSignal": "scr_stream_duplex_from_gen_signal", + "stream.duplexFromAsyncFnSignal": "scr_stream_duplex_from_async_fn_signal", "fetch.readableFromWeb": "scr_stream_readable_from_web", "writable.writeEncoding": "scr_stream_write_encoding", "writable.setDefaultEncoding": "scr_stream_set_default_encoding", @@ -1200,6 +1202,8 @@ export const LIB_FN_SYMS: Record = { "readable.opMap": "scr_stream_op_map", "readable.opFilter": "scr_stream_op_filter", "readable.opFlatMap": "scr_stream_op_flat_map", + "readable.opFlatMapAsyncGen": "scr_stream_op_flat_map_async_gen", + "readable.opFlatMapSyncGen": "scr_stream_op_flat_map_sync_gen", "readable.opDrop": "scr_stream_op_drop", "readable.opTake": "scr_stream_op_take", "readable.opToArray": "scr_stream_op_to_array", @@ -1208,6 +1212,11 @@ export const LIB_FN_SYMS: Record = { "readable.opEvery": "scr_stream_op_every", "readable.opFind": "scr_stream_op_find", "readable.opReduce": "scr_stream_op_reduce", + "readable.opSignalList": "scr_stream_op_signal_list", + "readable.opReduceSignal": "scr_stream_op_reduce_signal", + "readable.opSignalOf": "scr_stream_op_signal_of", + "readable.opOnceOptions": "scr_stream_op_once_options", + "readable.opSignalOpts": "scr_stream_op_signal_opts", "readable.pipe": "scr_stream_pipe", "readable.pipeGeneral": "scr_stream_pipe_general", "writable.cork": "scr_stream_cork", @@ -1711,6 +1720,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.opMap", "readable.opFilter", "readable.opFlatMap", + "readable.opFlatMapAsyncGen", + "readable.opFlatMapSyncGen", "readable.opDrop", "readable.opTake", "readable.opToArray", @@ -1719,6 +1730,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.opEvery", "readable.opFind", "readable.opReduce", + "readable.opReduceSignal", "readable.resume", "readable.pipe", "readable.pipeGeneral", @@ -1748,6 +1760,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.duplexifyPair", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", "fetch.readableFromWeb", "stream.setWritev", "stream.setWritevDefault", diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 462f15a9e..f84a1c213 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 31 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v31" as const; +export const RUNTIME_ABI_VERSION = 32 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v32" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 4292b93b2..f897a2c67 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, + runtime_abi: { version: 32, marker: "scr_runtime_abi_v32" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index a5cb5dabe..903ccfd4d 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, + runtime_abi: { version: 32, marker: "scr_runtime_abi_v32" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index af3212ef0..8a4a1e417 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -85,7 +85,7 @@ import { lowerDynamicEmitterOn, } from "./lower-event-emitter.js"; import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js"; -import { STREAM_OPERATORS } from "./lower-stream.js"; +import { STREAM_OPERATORS, operatorSignalOptions, reduceSignalSetup } from "./lower-stream.js"; import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js"; const STREAM_METHODS = new Set([ @@ -5640,13 +5640,16 @@ export class ClassDynamicDispatch { const readableT: IrType = { kind: "object", className: "%Readable" }; const op = (fn: string, args: IrExpr[], type: IrType): IrStmt[] => ret({ kind: "libCall", fn: fn as IrLibFn, args: [receiver, ...args], type, loc }); + // Node passes every callback its { signal }; the callback's + // declared parameters are not known here. + const sig = (): IrExpr => operatorSignalOptions(lowerer, arg(1), loc); switch (method) { case "map": - return op("readable.opMap", [arg(0), arg(1)], readableT); + return op("readable.opMap", [arg(0), arg(1), sig()], readableT); case "filter": - return op("readable.opFilter", [arg(0), arg(1)], readableT); + return op("readable.opFilter", [arg(0), arg(1), sig()], readableT); case "flatMap": - return op("readable.opFlatMap", [arg(0), arg(1)], readableT); + return op("readable.opFlatMap", [arg(0), arg(1), sig()], readableT); case "drop": return op("readable.opDrop", [arg(0), arg(1)], readableT); case "take": @@ -5654,25 +5657,49 @@ export class ClassDynamicDispatch { case "toArray": return op("readable.opToArray", [arg(0)], { kind: "promise", inner: DYN }); case "forEach": - return op("readable.opForEach", [arg(0), arg(1)], { kind: "promise", inner: VOID }); + return op("readable.opForEach", [arg(0), arg(1), sig()], { + kind: "promise", + inner: VOID, + }); case "some": - return op("readable.opSome", [arg(0), arg(1)], { kind: "promise", inner: BOOL }); + return op("readable.opSome", [arg(0), arg(1), sig()], { kind: "promise", inner: BOOL }); case "every": - return op("readable.opEvery", [arg(0), arg(1)], { kind: "promise", inner: BOOL }); + return op("readable.opEvery", [arg(0), arg(1), sig()], { + kind: "promise", + inner: BOOL, + }); case "find": - return op("readable.opFind", [arg(0), arg(1)], { kind: "promise", inner: DYN }); - case "reduce": - return op( - "readable.opReduce", - [ - arg(0), - arg(1), - { kind: "boolLit", value: dispatch.source.args.length >= 2, type: BOOL, loc }, - arg(2), - { kind: "numLit", value: 0, type: F64, loc }, - ], - { kind: "promise", inner: DYN }, - ); + return op("readable.opFind", [arg(0), arg(1), sig()], { kind: "promise", inner: DYN }); + case "reduce": { + // the dispatch function's locals: there is no function + // context while dispatch bodies are built + const setup = reduceSignalSetup(lowerer, arg(2), loc, (name, type) => { + const local = { + id: `${name}.${dispatch.fn.locals.length}`, + name, + type, + mutable: false, + }; + dispatch.fn.locals.push(local); + return local; + }); + return [ + ...setup.stmts, + ...op( + "readable.opReduceSignal", + [ + arg(0), + arg(1), + { kind: "boolLit", value: dispatch.source.args.length >= 2, type: BOOL, loc }, + arg(2), + { kind: "numLit", value: 0, type: F64, loc }, + setup.abort, + setup.cbOpts, + ], + { kind: "promise", inner: DYN }, + ), + ]; + } } } if (method === "destroy" && (!incoming.length || incoming[0]?.kind === "unitLit")) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 7d15e82bb..b6c6a42dd 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -3923,21 +3923,27 @@ function pipelineBoxHelper( /** `() => ac.abort()` over the pipeline's AbortController: an interned * lifted function takes the controller and answers the closure. */ -function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr { - const key = "pipelineAbort"; +function pipelineAbortHelper( + lowerer: Lowerer, + ac: IrExpr, + loc: SrcLoc, + withReason = false, +): IrExpr { + const key = withReason ? "pipelineAbortReason" : "pipelineAbort"; let name = lowerer.valueHelpers.get(key); - const abortT = funcOf([], VOID); + const abortT = funcOf(withReason ? [DYN] : [], VOID); if (!name) { name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); + const reasonLocal: IrLocal = { id: "reason.0", name: "reason", type: DYN, mutable: false }; lowerer.liftedFns.push({ name: `${name}.abort`, - params: [], + params: withReason ? [{ localId: reasonLocal.id, name: reasonLocal.name, type: DYN }] : [], returnType: VOID, captures: [{ localId: acLocal.id, name: acLocal.name, type: DYN }], - locals: [acLocal], + locals: withReason ? [acLocal, reasonLocal] : [acLocal], body: [ { kind: "exprStmt", @@ -3946,7 +3952,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr recv: acRef, method: "abort", calleeName: "ac.abort", - args: [], + args: withReason ? [varRef(reasonLocal.id, DYN, loc)] : [], type: DYN, loc, }, @@ -4165,11 +4171,11 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE "write the body as async function* (source) { ... } or async function (source) { ... }", ); } - if (fn.parameters.nodes.length > 1) { + if (fn.parameters.nodes.length > 2) { lowerer.noLowering( - "a Duplex.from body that declares the options argument", - fn.parameters.nodes[1]!, - "the body takes only its source", + "a Duplex.from body with parameters after the options argument", + fn.parameters.nodes[2]!, + "the body takes the source and { signal }", ); } const sourceT: SyncGenType = { @@ -4180,6 +4186,46 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE nextT: UNDEFINED_T, }; const lowered = lowerStageFunction(lowerer, fn, sourceT); + const params = lowered.closure.type.kind === "func" ? lowered.closure.type.params : []; + // fromAsyncGen calls the body with { signal } of an AbortController + // that destroy aborts with its error. + let signalArgs: IrExpr[] = []; + let signalDecls: IrStmt[] = []; + if (params.length > 1) { + if (params[1]!.kind !== "dyn") { + lowerer.noLowering( + "a Duplex.from body's options argument with this type", + fn.parameters.nodes[1]!, + "type it as { signal: AbortSignal }", + ); + } + const acLocal = lowerer.declareHiddenLocal("%duplexAbort", DYN); + const ac = varRef(acLocal.id, DYN, loc); + signalDecls = [ + { + kind: "varDecl", + localId: acLocal.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + ]; + signalArgs = [ + pipelineAbortHelper(lowerer, ac, loc, true), + { kind: "dynKeyGet", value: ac, key: strLit("signal", loc), type: DYN, loc }, + ]; + } + const withSignal = (fnName: IrLibFn, args: IrExpr[]): IrExpr => { + const call: IrExpr = { + kind: "libCall", + fn: signalArgs.length > 0 ? (`${fnName}Signal` as IrLibFn) : fnName, + args: [...args, ...signalArgs], + type: duplex, + loc, + }; + return signalDecls.length > 0 + ? { kind: "seqExpr", stmts: signalDecls, result: call, type: duplex, loc } + : call; + }; if (!isGen) { // an async function: a writable Duplexify whose final waits for // the function's promise @@ -4200,26 +4246,18 @@ function duplexFromBody(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrE node, ); } - return { - kind: "libCall", - fn: "stream.duplexFromAsyncFn", - args: [lowered.closure, asyncSource, pipelineBoxHelper(lowerer, out, loc)], - type: duplex, - loc, - }; + return withSignal("stream.duplexFromAsyncFn", [ + lowered.closure, + asyncSource, + pipelineBoxHelper(lowerer, out, loc), + ]); } const helpers = lowered.out.kind === "generator" ? fromGenHelpers(lowerer, lowered.out, loc) : null; const source = duplexSourceHelper(lowerer, sourceT, loc); if (!helpers || !source) lowerer.noLowering("Duplex.from over a function with this generator type", node); - return { - kind: "libCall", - fn: "stream.duplexFromGen", - args: [lowered.closure, source!, ...helpers!], - type: duplex, - loc, - }; + return withSignal("stream.duplexFromGen", [lowered.closure, source!, ...helpers!]); } // duplexify.js: a stream answers itself (a Duplex) or is wrapped; any // other body reads as from(Duplexify, body, { objectMode: true, @@ -4510,39 +4548,38 @@ export const STREAM_OPERATORS: ReadonlySet = new Set([ "reduce", ]); -/** An operator callback as a checked-dynamic function value. Callbacks - * that declare the options argument ({ signal }) are refused: the - * AbortSignal Node passes there has no representation. map and flatMap - * callbacks must produce strings or Buffers (flatMap: arrays of them, or - * strings) — or checked-dynamic values, which the runtime checks. */ +/** An operator callback as a checked-dynamic function value, and whether + * it declares the options argument ({ signal }), which the runtime then + * passes. Its results convert to checked-dynamic values (flatMap's are + * iterated with yield*). */ function lowerOperatorCallback( lowerer: Lowerer, node: ts.Expression, member: string, maxParams: number, -): IrExpr { +): { fn: IrExpr; takesOptions: boolean; gen?: IrType & { kind: "generator" } } { const loc = locOf(node); const optionsParam = (at: ts.Node): never => lowerer.noLowering( - `${member} callbacks that declare the options argument`, + `${member} callbacks with parameters after the options argument`, at, - "the { signal } argument has no lowering — take only the chunk", + "the callback takes the chunk and { signal }", ); - if ( - (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && - node.parameters.nodes.length > maxParams - ) { - optionsParam(node.parameters.nodes[maxParams]!); + let takesOptions = false; + if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + if (node.parameters.nodes.length > maxParams + 1) + optionsParam(node.parameters.nodes[maxParams + 1]!); + takesOptions = node.parameters.nodes.length > maxParams; } const v = lowerer.lowerExpr(node); - if (v.type.kind === "dyn") return v; + if (v.type.kind === "dyn") return { fn: v, takesOptions }; if ( v.type.kind !== "func" && isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type) ) { // JavaScript passes any value; Node's validateFunction throws. - return { kind: "dynFrom", value: v, type: DYN, loc }; + return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions: false }; } if (v.type.kind !== "func") { lowerer.noLowering( @@ -4551,25 +4588,8 @@ function lowerOperatorCallback( "pass a function", ); } - if (v.type.params.length > maxParams) optionsParam(node); - if (member === "map" || member === "flatMap") { - const ret = v.type.ret.kind === "promise" ? v.type.ret.inner : v.type.ret; - const chunk = (t: IrType): boolean => - t.kind === "string" || t.kind === "dyn" || (t.kind === "bytes" && t.elem === "u8"); - const ok = - member === "map" - ? chunk(ret) - : ret.kind === "dyn" || ret.kind === "string" || (ret.kind === "array" && chunk(ret.elem)); - if (!ok) { - lowerer.noLowering( - `${member} callbacks that return '${lowerer.fmt(ret)}'`, - node, - member === "map" - ? "map results are stream chunks: strings or Buffers (object-mode streams of other values are not lowered)" - : "flatMap results are arrays of strings or Buffers, or strings", - ); - } - } + if (v.type.params.length > maxParams + 1) optionsParam(node); + if (v.type.params.length > maxParams) takesOptions = true; const getRecord = (id: string) => lowerer.shapes.get(id); const getUnion = (id: string) => lowerer.unions.get(id); if (!canBoxFuncIntoDyn(v.type, getRecord, getUnion)) { @@ -4579,7 +4599,95 @@ function lowerOperatorCallback( "the callback's parameter and result types must cross the checked-dynamic boundary", ); } - return { kind: "dynFrom", value: v, type: DYN, loc }; + // flatMap over a compiled generator: the runtime steps it through the + // helpers over its type + const gen = member === "flatMap" && v.type.ret.kind === "generator" ? v.type.ret : undefined; + return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions, ...(gen && { gen }) }; +} + +/** The operator callbacks' `{ signal }` (operators.js): map and the + * operators over it pass `{ signal: AbortSignal.any([options?.signal] + * .filter(Boolean)) }`. options is the operator's options value. */ +export function operatorSignalOptions(lowerer: Lowerer, options: IrExpr, loc: SrcLoc): IrExpr { + void lowerer; + const list: IrExpr = { + kind: "libCall", + fn: "readable.opSignalList", + args: [options], + type: DYN, + loc, + }; + const signal: IrExpr = { kind: "libCall", fn: "fetch.abortAny", args: [list], type: DYN, loc }; + return { kind: "libCall", fn: "readable.opSignalOpts", args: [signal], type: DYN, loc }; +} + +/** reduce's signal for a reducer that reads it (operators.js): `const ac + * = new AbortController()`, `options.signal.addEventListener('abort', () + * => ac.abort(), { once: true })`, then reduceSignal's ac.abort() closure + * and `{ signal: ac.signal }`. options is the bound options value. */ +export function reduceSignalSetup( + lowerer: Lowerer, + options: IrExpr, + loc: SrcLoc, + declare: (name: string, type: IrType) => IrLocal = (name, type) => + lowerer.declareHiddenLocal(name, type), +): { stmts: IrStmt[]; abort: IrExpr; cbOpts: IrExpr } { + const acLocal = declare("%opAbort", DYN); + const ac = varRef(acLocal.id, DYN, loc); + const sigLocal = declare("%opUserSignal", DYN); + const sig = varRef(sigLocal.id, DYN, loc); + const abort = pipelineAbortHelper(lowerer, ac, loc); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: acLocal.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + { + kind: "varDecl", + localId: sigLocal.id, + init: { kind: "libCall", fn: "readable.opSignalOf", args: [options], type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "truthy", value: sig, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { + kind: "dynInvoke", + recv: sig, + method: "addEventListener", + calleeName: "options.signal.addEventListener", + args: [ + { kind: "dynFrom", value: strLit("abort", loc), type: DYN, loc }, + { kind: "dynFrom", value: pipelineAbortHelper(lowerer, ac, loc), type: DYN, loc }, + { kind: "libCall", fn: "readable.opOnceOptions", args: [], type: DYN, loc }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ]; + const signal: IrExpr = { + kind: "dynKeyGet", + value: ac, + key: strLit("signal", loc), + type: DYN, + loc, + }; + return { + stmts, + abort, + cbOpts: { kind: "libCall", fn: "readable.opSignalOpts", args: [signal], type: DYN, loc }, + }; } /** An operator's options (or reduce's initial value): an object literal @@ -4618,21 +4726,37 @@ function lowerStreamOperator( ): IrExpr { const loc = locOf(call); const args = call.arguments.nodes; - const recv = streamReceiverExpr(lowerer, access.expression); + // JS order: the receiver, the callback, then the options; the + // callback's { signal } is built over the options value afterwards. + const decls: IrStmt[] = []; + const bind = (value: IrExpr, name: string): IrExpr => { + const local = lowerer.declareHiddenLocal(name, value.type); + decls.push({ kind: "varDecl", localId: local.id, init: value, loc }); + return varRef(local.id, value.type, loc); + }; + const recv = bind(streamReceiverExpr(lowerer, access.expression), "%opRecv"); const readable: IrType = { kind: "object", className: "%Readable" }; const opts = (i: number): IrExpr => lowerOperatorValue(lowerer, args[i], `${member} options`, loc, true); + let takesOptions = false; + let callbackGen: (IrType & { kind: "generator" }) | undefined; const fnArg = (maxParams: number): IrExpr => { if (args[0] === undefined) return dynUndefinedExpr(loc); - return lowerOperatorCallback(lowerer, args[0], member, maxParams); + const lowered = lowerOperatorCallback(lowerer, args[0], member, maxParams); + takesOptions = lowered.takesOptions; + callbackGen = lowered.gen; + return bind(lowered.fn, "%opFn"); + }; + const lib = (fn: string, a: IrExpr[], type: IrType): IrExpr => { + const call: IrExpr = { kind: "libCall", fn: fn as IrLibFn, args: a, type, loc }; + return { kind: "seqExpr", stmts: decls, result: call, type, loc }; + }; + // the options argument (bound), and the callback's { signal } when it + // declares one + const withSignal = (optsValue: IrExpr): IrExpr[] => { + const o = bind(optsValue, "%opOptions"); + return [o, takesOptions ? operatorSignalOptions(lowerer, o, loc) : dynUndefinedExpr(loc)]; }; - const lib = (fn: string, a: IrExpr[], type: IrType): IrExpr => ({ - kind: "libCall", - fn: fn as IrLibFn, - args: a, - type, - loc, - }); switch (member) { case "map": case "filter": @@ -4644,7 +4768,16 @@ function lowerStreamOperator( : member === "filter" ? "readable.opFilter" : "readable.opFlatMap"; - return lib(fnName, [recv, fn, opts(1)], readable); + const rest = withSignal(opts(1)); + const genHelpers = callbackGen ? fromGenHelpers(lowerer, callbackGen, loc) : null; + if (callbackGen && genHelpers) { + return lib( + callbackGen.async ? "readable.opFlatMapAsyncGen" : "readable.opFlatMapSyncGen", + [recv, fn, ...rest, ...genHelpers], + readable, + ); + } + return lib(fnName, [recv, fn, ...rest], readable); } case "drop": case "take": { @@ -4657,15 +4790,24 @@ function lowerStreamOperator( } case "toArray": return lib("readable.opToArray", [recv, opts(0)], { kind: "promise", inner: DYN }); - case "forEach": - return lib("readable.opForEach", [recv, fnArg(1), opts(1)], { kind: "promise", inner: VOID }); + case "forEach": { + const fn = fnArg(1); + return lib("readable.opForEach", [recv, fn, ...withSignal(opts(1))], { + kind: "promise", + inner: VOID, + }); + } case "some": case "every": { const fn = fnArg(1); - return lib(member === "some" ? "readable.opSome" : "readable.opEvery", [recv, fn, opts(1)], { - kind: "promise", - inner: BOOL, - }); + return lib( + member === "some" ? "readable.opSome" : "readable.opEvery", + [recv, fn, ...withSignal(opts(1))], + { + kind: "promise", + inner: BOOL, + }, + ); } case "find": { const t = lowerer.typeOf(call); @@ -4677,7 +4819,11 @@ function lowerStreamOperator( "the result is a checked-dynamic value — use a boolean predicate and narrow the result", ); } - return lib("readable.opFind", [recv, fnArg(1), opts(1)], { kind: "promise", inner: DYN }); + const fn = fnArg(1); + return lib("readable.opFind", [recv, fn, ...withSignal(opts(1))], { + kind: "promise", + inner: DYN, + }); } case "reduce": { const t = lowerer.typeOf(call); @@ -4704,12 +4850,19 @@ function lowerStreamOperator( } } const fn = fnArg(2); - const init = lowerOperatorValue(lowerer, args[1], "reduce's initial value", loc, false); - return lib( - "readable.opReduce", - [recv, fn, init, boolLit(args.length >= 2, loc), opts(2), numLit(kind, loc)], - { kind: "promise", inner }, + const init = bind( + lowerOperatorValue(lowerer, args[1], "reduce's initial value", loc, false), + "%opInit", ); + const o = bind(opts(2), "%opOptions"); + const head = [recv, fn, init, boolLit(args.length >= 2, loc), o, numLit(kind, loc)]; + if (!takesOptions) return lib("readable.opReduce", head, { kind: "promise", inner }); + const setup = reduceSignalSetup(lowerer, o, loc); + decls.push(...setup.stmts); + return lib("readable.opReduceSignal", [...head, setup.abort, setup.cbOpts], { + kind: "promise", + inner, + }); } } throw new InternalCompilerError(`unknown stream operator ${member}`); @@ -5253,6 +5406,22 @@ export function lowerStreamMethodCall( requireSide(readableSide, "read"); if (args.length > 1) lowerer.noLowering(`read with ${args.length} arguments`, call); const receiver = streamReceiverExpr(lowerer, access.expression); + // JavaScript (and a result the declarations type as any) reads any + // chunk: a Buffer, a string once encoded, or an object-mode value. + const resultTs = lowerer.typeOf(call); + if ( + isJsSourceFile(ts.getSourceFileOfNode(call)) || + (resultTs.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ) { + const sizeDyn = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "readable.readDyn", + args: [receiver, sizeDyn], + type: DYN, + loc, + }; + } const size = args[0] ? lowerer.lowerExprExpecting(args[0], F64) : numLit(-1, loc); const type: IrType = unionOf(lowerer, [BYTES, { kind: "nullT" }]); const read: IrExpr = { diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index af2f2a7cc..3e145f485 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -386,6 +386,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "stream.duplexFromDyn", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", "fetch.readableFromWeb", "stream.destroy", "stream.destroyErr", @@ -521,6 +523,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.opMap", "readable.opFilter", "readable.opFlatMap", + "readable.opFlatMapAsyncGen", + "readable.opFlatMapSyncGen", "readable.opDrop", "readable.opTake", "rl.createSignal", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9b23a40da..68c3eec77 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4082,6 +4082,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** Duplex.from(asyncFunction): the stage, its source helper and the * promise's box helper. */ "stream.duplexFromAsyncFn": { argTypes: [null, null, null], result: VOID }, + /** The same for a body that declares the options argument: then the + * AbortController's abort() and its signal. */ + "stream.duplexFromGenSignal": { + argTypes: [null, null, null, null, null, null, null, DYN], + result: VOID, + }, + "stream.duplexFromAsyncFnSignal": { argTypes: [null, null, null, null, DYN], result: VOID }, "writable.writeEncoding": { argTypes: [null, STRING], result: VOID }, "writable.setDefaultEncoding": { argTypes: [null, DYN], result: VOID }, /** Whether a readable.nextChunkDyn step answered the end (the done @@ -4119,17 +4126,39 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * forEach nothing; reduce: [stream, fn, init, hasInit, options, kind] * with the payload kind (0 dyn, 1 number, 2 boolean, 3 string). The * promise-returning ones reject instead of throwing. */ - "readable.opMap": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opFilter": { argTypes: [null, DYN, DYN], result: VOID }, - "readable.opFlatMap": { argTypes: [null, DYN, DYN], result: VOID }, + "readable.opMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFilter": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + "readable.opFlatMap": { argTypes: [null, DYN, DYN, DYN], result: VOID }, + /** flatMap whose callback returns a compiled generator: the helpers + * over its type follow (async: next, read, return, throw; sync: next, + * return, throw). */ + "readable.opFlatMapAsyncGen": { + argTypes: [null, DYN, DYN, DYN, null, null, null, null], + result: VOID, + }, + "readable.opFlatMapSyncGen": { argTypes: [null, DYN, DYN, DYN, null, null, null], result: VOID }, "readable.opDrop": { argTypes: [null, DYN, DYN], result: VOID }, "readable.opTake": { argTypes: [null, DYN, DYN], result: VOID }, "readable.opToArray": { argTypes: [null, DYN], result: { kind: "promise", inner: DYN } }, - "readable.opForEach": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: VOID } }, - "readable.opSome": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opEvery": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: BOOL } }, - "readable.opFind": { argTypes: [null, DYN, DYN], result: { kind: "promise", inner: DYN } }, + "readable.opForEach": { + argTypes: [null, DYN, DYN, DYN], + result: { kind: "promise", inner: VOID }, + }, + "readable.opSome": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, + "readable.opEvery": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: BOOL } }, + "readable.opFind": { argTypes: [null, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } }, + /** The map family's last argument is the callback's { signal } (or + * undefined). reduceSignal adds ac.abort() (a closure) and + * { signal: ac.signal } for a reducer that reads them. */ "readable.opReduce": { argTypes: [null, DYN, DYN, BOOL, DYN, F64], result: VOID }, + "readable.opReduceSignal": { + argTypes: [null, DYN, DYN, BOOL, DYN, F64, null, DYN], + result: VOID, + }, + "readable.opSignalList": { argTypes: [DYN], result: DYN }, + "readable.opSignalOpts": { argTypes: [DYN], result: DYN }, + "readable.opSignalOf": { argTypes: [DYN], result: DYN }, + "readable.opOnceOptions": { argTypes: [], result: DYN }, "readable.resume": { argTypes: [null], result: VOID }, "readable.isPaused": { argTypes: [null], result: BOOL }, "readable.pipe": { argTypes: [null, null, BOOL], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 12baeea57..68020a1ce 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -223,6 +223,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.duplexFromDyn", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", "fetch.readableFromWeb", "stream.duplexifyReadable", "stream.duplexifyWritable", @@ -285,6 +287,8 @@ const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "stream.duplexFromDyn", "stream.duplexFromPromise", "stream.duplexFromAsyncFn", + "stream.duplexFromGenSignal", + "stream.duplexFromAsyncFnSignal", ]); /** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 8c9a0432e..6e8c50ff1 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5520,6 +5520,8 @@ function validateFunction( e.fn === "stream.duplexifyPair" || e.fn === "stream.duplexFromPromise" || e.fn === "stream.duplexFromAsyncFn" || + e.fn === "stream.duplexFromGenSignal" || + e.fn === "stream.duplexFromAsyncFnSignal" || e.fn === "stream.setConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" || @@ -5597,7 +5599,9 @@ function validateFunction( if ( e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn" || - e.fn === "stream.duplexFromAsyncFn" + e.fn === "stream.duplexFromAsyncFn" || + e.fn === "stream.duplexFromGenSignal" || + e.fn === "stream.duplexFromAsyncFnSignal" ) { // Duplex.from: (stage, source, helpers...) or (body) → a Duplex. if (!isStreamObject(e.type)) { @@ -5786,6 +5790,8 @@ function validateFunction( e.fn === "readable.opMap" || e.fn === "readable.opFilter" || e.fn === "readable.opFlatMap" || + e.fn === "readable.opFlatMapAsyncGen" || + e.fn === "readable.opFlatMapSyncGen" || e.fn === "readable.opDrop" || e.fn === "readable.opTake" ) { @@ -5794,13 +5800,30 @@ function validateFunction( } return true; } - if (e.fn === "readable.opReduce") { + if (e.fn === "readable.opReduce" || e.fn === "readable.opReduceSignal") { const inner = e.type.kind === "promise" ? e.type.inner : null; const kind = e.args[5]?.kind === "numLit" ? e.args[5].value : -1; const want = [DYN, F64, BOOL, STRING][kind]; if (!isStreamObject(e.args[0]?.type) || !inner || !want || !typeEquals(inner, want)) { err(`libCall readable.opReduce must return the promise its kind names`, e.loc); } + if (e.fn === "readable.opReduceSignal" && e.args[6]?.type.kind !== "func") + err(`libCall ${e.fn} takes ac.abort() as a closure`, e.loc); + return true; + } + if (e.fn === "readable.opOnceOptions") { + if (e.args.length !== 0 || e.type.kind !== "dyn") + err(`libCall ${e.fn} takes no arguments`, e.loc); + return true; + } + if ( + e.fn === "readable.opSignalList" || + e.fn === "readable.opSignalOpts" || + e.fn === "readable.opSignalOf" + ) { + // The operator callbacks' { signal }: (dyn) → dyn. + if (e.args.length !== 1 || e.args[0]?.type.kind !== "dyn" || e.type.kind !== "dyn") + err(`libCall ${e.fn} takes and returns a checked-dynamic value`, e.loc); return true; } if (e.fn === "readable.fromDone") { diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 7f819bdad..720de5d6d 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v31"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v32"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v31"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v32"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json new file mode 100644 index 000000000..5de5d9839 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7433-readable-operator-signal.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7433-readable-operator-signal.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json new file mode 100644 index 000000000..c374d9f03 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7434-readable-operator-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7434-readable-operator-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json new file mode 100644 index 000000000..0c3697807 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7435-readable-flatmap-iterables.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7435-readable-flatmap-iterables.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json new file mode 100644 index 000000000..6c2f63e17 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7436-readable-read-any-value.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7436-readable-read-any-value.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json new file mode 100644 index 000000000..3b0699407 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7437-duplex-from-signal.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7437-duplex-from-signal.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json new file mode 100644 index 000000000..0702ed0da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7438-duplex-from-signal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7438-duplex-from-signal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json new file mode 100644 index 000000000..e3dab7078 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7439-readable-read-string-chunks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7439-readable-read-string-chunks.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e04a0dfcf..760a60553 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, + runtime_abi: { version: 32, marker: "scr_runtime_abi_v32" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index dd34cf10b..dc4dc74ef 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, + runtime_abi: { version: 32, marker: "scr_runtime_abi_v32" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 194a14b26..e24949ad3 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v31(void) {} +void scr_runtime_abi_v32(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 9f27a15b1..3d11868a2 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v31(void); +void scr_runtime_abi_v32(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index cdc51466e..d26077b5c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -431,18 +431,41 @@ int scr_stream_push_object(ScrStream *s, const ScrDyn *v); * dyn array, find and reduce a dyn value — reduce's `kind` picks the * payload instead: 0 dyn, 1 number, 2 boolean, 3 string — some and every * a boolean, forEach nothing. */ -ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options); +ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +/* flatMap whose callback returns a compiled generator: the helpers over its + * type (async: next, read, return, throw; sync: next, return, throw; + * borrowed). */ +ScrStream *scr_stream_op_flat_map_async_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *read, + ScrClosure *ret, ScrClosure *thr); +ScrStream *scr_stream_op_flat_map_sync_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *ret, + ScrClosure *thr); ScrStream *scr_stream_op_drop(ScrStream *s, ScrDyn *number, ScrDyn *options); ScrStream *scr_stream_op_take(ScrStream *s, ScrDyn *number, ScrDyn *options); ScrPromise *scr_stream_op_to_array(ScrStream *s, ScrDyn *options); -ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options); -ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options); +ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); +ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts); ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, ScrDyn *options, double kind); +/* reduce whose reducer reads its { signal } (cb_opts): abort is ac.abort() + * (borrowed), run when the reduction ends. */ +ScrPromise *scr_stream_op_reduce_signal(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *abort, + ScrDyn *cb_opts); +/* `options?.signal` (+1, undefined when absent) and `{ once: true }` (+1), + * for the listener that makes options.signal abort reduce's controller. */ +ScrDyn *scr_stream_op_signal_of(ScrDyn *options); +ScrDyn *scr_stream_op_once_options(void); +/* The operator callbacks' { signal } (the options argument): the signals + * AbortSignal.any combines (`[options?.signal].filter(Boolean)`) and the + * object around the combined signal. +1 each. */ +ScrDyn *scr_stream_op_signal_list(ScrDyn *options); +ScrDyn *scr_stream_op_signal_opts(ScrDyn *signal); /* finished(s, options[, cb]) — Node's eos with its listeners and state * predicates: readable/writable are -1 (the default), 0 or 1; on_error is * options.error !== false; signal is NULL or borrowed (ERR_INVALID_ARG_TYPE @@ -556,6 +579,16 @@ ScrStream *scr_stream_readable_from_web(struct ScrDyn *web, bool object_mode, do * generator and answers its promise; source and the promise's box helper * borrowed): a writable Duplexify whose final waits for the promise. +1. */ ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box); +/* The same for a body that declares the options argument: it takes + * { signal } (signal borrowed), and destroy calls abort(err) (borrowed), + * the AbortController's abort, before it settles (fromAsyncGen's + * ac.abort(err)). */ +ScrStream *scr_stream_duplex_from_gen_signal(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, struct ScrDyn *signal); +ScrStream *scr_stream_duplex_from_async_fn_signal(ScrClosure *stage, ScrClosure *source, + ScrClosure *box, ScrClosure *abort, + struct ScrDyn *signal); /* Duplex.from over a Readable, a Writable, or a { readable, writable } * pair (duplexify.js's _duplexify): a Duplexify that reads r and writes w * (either NULL in scr_stream_duplexify; streams borrowed). +1. */ diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index 75ce416ac..a92b4a0e5 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -267,20 +267,15 @@ static bool op_aborted(const ScrDyn *signal) { return r; } -/* The callback argument: a function that does not read the options - * argument. `max` is how many parameters Node's call passes before the - * options. False with the error pending. */ +/* The callback argument: validateFunction. False with the error pending. + * (The options argument the callback may read is the operator's + * cb_opts.) */ static bool op_check_fn(const ScrDyn *fn, const char *name, uint32_t max) { + (void)max; if (!fn || !scr_dyn_is_callable(fn)) { scr_dyn_arg_type_fail(name, "of type function", fn ? fn : scr_dyn_undefined()); return false; } - if (fn->kind == SCR_DYN_FUNC && fn->v.fn.arity > max) { - static const char m[] = - "stream operator callbacks that read the options argument ({ signal }) are not supported yet"; - scr_throw_error_msg(SCR_ERR_ERROR, m, sizeof m - 1); - return false; - } return true; } @@ -610,8 +605,9 @@ enum { MODE_MAP, MODE_FILTER, MODE_FOREACH, MODE_EVERY }; typedef struct { OpGen g; OpSrc src; - ScrDyn *fn; /* +1 */ - ScrDyn *signal; /* +1 or NULL */ + ScrDyn *fn; /* +1 */ + ScrDyn *signal; /* +1 or NULL */ + ScrDyn *cb_opts; /* +1: the { signal } fn takes as its second argument, or NULL */ int mode; double concurrency, hwm; ScrDyn **queue; /* +1 entries: dyn promises or kEof */ @@ -628,6 +624,7 @@ static void op_map_body_drop(OpGen *g) { src_release(&m->src); scr_dyn_release(m->fn); if (m->signal) scr_dyn_release(m->signal); + if (m->cb_opts) scr_dyn_release(m->cb_opts); for (size_t i = m->qh; i < m->qn; i++) scr_dyn_release(m->queue[i]); free(m->queue); if (m->next_p) scr_promise_release(m->next_p); @@ -734,9 +731,11 @@ static void op_wrap_k(void *self, ScrDyn *v) { } /* Calls fn(chunk): +1 result, or NULL with the exception pending. */ -static ScrDyn *op_call(ScrDyn *fn, ScrDyn *chunk) { - ScrDyn *args[1] = {chunk}; - return scr_dyn_call(fn, args, 1, "fn"); +/* Calls fn(chunk, signalOpt) — the second argument when the callback + * declares it. +1 result, or NULL with the exception pending. */ +static ScrDyn *op_call(OpMap *m, ScrDyn *chunk) { + ScrDyn *args[2] = {chunk, m->cb_opts}; + return scr_dyn_call(m->fn, args, m->cb_opts ? 2 : 1, "fn"); } /* An async wrapper body: `await fn(chunk)` then its transform. The @@ -744,7 +743,7 @@ static ScrDyn *op_call(ScrDyn *fn, ScrDyn *chunk) { static ScrPromise *op_wrap_call(OpMap *m, ScrDyn *chunk, int mode, int stage) { OpWrap *w = op_wrap_new(mode, chunk, stage); ScrPromise *result = scr_promise_retain(w->p); - ScrDyn *r = op_call(m->fn, chunk); + ScrDyn *r = op_call(m, chunk); if (!r) { scr_promise_reject_pending(w->p); } else { @@ -772,7 +771,7 @@ static ScrPromise *op_every_call(OpMap *m, ScrDyn *chunk) { static ScrDyn *map_item(OpMap *m, ScrDyn *chunk) { ScrPromise *p; if (m->mode == MODE_MAP) { - ScrDyn *r = op_call(m->fn, chunk); + ScrDyn *r = op_call(m, chunk); if (!r) { ScrCaught *c = scr_exc_take(); ScrDyn *d = op_rejected_dyn(c); @@ -981,7 +980,7 @@ static void map_thr(OpGen *g, ScrCaught *c) { } static OpMap *map_new(ScrStream *s, ScrDyn *fn, int mode, double concurrency, double hwm, - ScrDyn *signal) { + ScrDyn *signal, ScrDyn *cb_opts) { OpMap *m = op_alloc(sizeof *m); gen_init(&m->g, &op_map_body_drop); m->g.resume = &map_resume; @@ -990,6 +989,7 @@ static OpMap *map_new(ScrStream *s, ScrDyn *fn, int mode, double concurrency, do m->src.s = scr_stream_retain(s); m->fn = scr_dyn_retain(fn); m->signal = signal ? scr_dyn_retain(signal) : NULL; + m->cb_opts = cb_opts && cb_opts->kind != SCR_DYN_UNDEF ? scr_dyn_retain(cb_opts) : NULL; m->mode = mode; m->concurrency = concurrency; m->hwm = hwm; @@ -1137,27 +1137,137 @@ static void slice_thr(OpGen *g, ScrCaught *c) { * goes through the async-from-sync iterator: each step resolves after the * element's PromiseResolve job, and yield* awaits that result before * yielding without a further await. */ +/* yield* over the delegate: an array or string (its elements, through + * the async-from-sync iterator), another iterable's iterator (FLAT_SYNC + * through the async-from-sync iterator, FLAT_ASYNC awaiting next()), or a + * native Readable (FLAT_STREAM, its async iterator). */ +enum { FLAT_NONE, FLAT_SYNC, FLAT_ASYNC, FLAT_STREAM, FLAT_GEN_SYNC, FLAT_GEN_ASYNC }; + +typedef ScrDyn *(*FromSyncFn)(ScrClosure *, ScrGen *); +typedef ScrDyn *(*FromSyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrPromise *(*FromAsyncFn)(ScrClosure *, ScrGen *); +typedef ScrPromise *(*FromAsyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); +typedef ScrDyn *(*FromReadFn)(ScrClosure *, void *); +static void op_await_gen(ScrPromise *p, ScrClosure *read, OpCont k, void *self); + typedef struct { OpGen g; OpSrc values; /* the map generator */ ScrDyn *inner; /* +1: the array or string being delegated to, or NULL */ size_t idx; /* element index, or the string's byte offset */ ScrPromise *step; /* the async-from-sync iterator result being awaited */ + ScrDyn *iter; /* +1: the delegate's iterator (FLAT_SYNC / FLAT_ASYNC), or NULL */ + ScrStream *stream; /* +1: the delegate stream (FLAT_STREAM), or NULL */ + ScrGen *gen; /* +1: a compiled generator delegate (FLAT_GEN_*), or NULL */ + /* The compiler's helpers over the callback's generator type (+1 each, + * or NULL): next/return/throw, and read for an async generator. */ + ScrClosure *gnext, *gread, *gret, *gthr; + bool gen_async; + int iter_kind; bool started; } OpFlat; +/* A delegate's member (+1), read as the IR's keyed read does: a compiled + * value (a generator, its result record) through its typed ref, anything + * else through the iterator-result read. NULL with the error pending. */ +static ScrDyn *flat_get(const ScrDyn *v, const char *key) { + if (v->kind == SCR_DYN_TYPED_REF) { + ScrStr *k = scr_str_new(key, strlen(key)); + ScrDyn *r = scr_dyn_typed_ref_key_get(v, k); + scr_str_release(k); + return r; + } + return scr_dyn_iterator_result_get(v, key, strlen(key)); +} + +/* iterator[name](...args) with the iterator as this: +1, or NULL with the + * error pending. */ +static ScrDyn *flat_call(const ScrDyn *iter, const char *name, ScrDyn *const *args, size_t argc) { + ScrDyn *m = flat_get(iter, name); + if (!m) return NULL; + scr_dyn_this_push_dyn(iter); + ScrDyn *r = scr_dyn_call(m, args, argc, name); + scr_dyn_this_pop(); + scr_dyn_release(m); + return r; +} + +/* `const { value, done } = result`: +1 value with *done set, or NULL with + * the error pending. */ +static ScrDyn *flat_result(const ScrDyn *r, bool *done) { + if (r->kind == SCR_DYN_UNDEF || r->kind == SCR_DYN_NULL || r->kind == SCR_DYN_NUM || + r->kind == SCR_DYN_STR || r->kind == SCR_DYN_BOOL) { + ScrDyn *checked = scr_dyn_iterator_result((ScrDyn *)r); /* throws V8's TypeError */ + if (checked) scr_dyn_release(checked); + return NULL; + } + ScrDyn *d = flat_get(r, "done"); + if (!d) return NULL; + *done = scr_dyn_truthy(d); + scr_dyn_release(d); + return flat_get(r, "value"); +} + +static void flat_drop_delegate(OpFlat *f) { + if (f->inner) scr_dyn_release(f->inner); + if (f->iter) scr_dyn_release(f->iter); + if (f->stream) scr_stream_release(f->stream); + if (f->gen) scr_gen_release(f->gen); + f->inner = f->iter = NULL; + f->stream = NULL; + f->gen = NULL; + f->iter_kind = FLAT_NONE; +} + static void op_flat_body_drop(OpGen *g) { OpFlat *f = (OpFlat *)g; src_release(&f->values); - if (f->inner) scr_dyn_release(f->inner); + flat_drop_delegate(f); if (f->step) scr_promise_release(f->step); + if (f->gnext) scr_closure_release(f->gnext); + if (f->gread) scr_closure_release(f->gread); + if (f->gret) scr_closure_release(f->gret); + if (f->gthr) scr_closure_release(f->gthr); free(f); } static void flat_next_value(OpFlat *f); -/* The next element (+1), or NULL when the delegate is exhausted. */ -static ScrDyn *flat_take_element(OpFlat *f) { +/* The next element (+1), or NULL when the delegate is exhausted; for a + * sync iterator NULL with *failed set when next() threw (pending). */ +static ScrDyn *flat_take_element(OpFlat *f, bool *failed) { + *failed = false; + if (f->iter_kind == FLAT_GEN_SYNC) { + ScrDyn *v = ((FromSyncFn)f->gnext->fn)(f->gnext, scr_gen_retain(f->gen)); + if (!v) { + *failed = true; + return NULL; + } + if (v == op_k_done) { + scr_dyn_release(v); + return NULL; + } + return v; + } + if (f->iter_kind == FLAT_SYNC) { + ScrDyn *r = flat_call(f->iter, "next", NULL, 0); + if (!r) { + *failed = true; + return NULL; + } + bool done = false; + ScrDyn *v = flat_result(r, &done); + scr_dyn_release(r); + if (!v) { + *failed = true; + return NULL; + } + if (done) { + scr_dyn_release(v); + return NULL; + } + return v; + } ScrDyn *in = f->inner; if (in->kind == SCR_DYN_ARR) { if (f->idx >= in->v.arr.len) return NULL; @@ -1185,10 +1295,15 @@ static void flat_step_k(void *self, ScrDyn *v) { scr_promise_release(f->step); f->step = NULL; } - if (v == op_k_done || (v && v == op_k_eof)) { + if (!v) { + /* the delegate's next() threw: it leaves the for await */ + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done || v == op_k_eof) { scr_dyn_release(v); - scr_dyn_release(f->inner); - f->inner = NULL; + flat_drop_delegate(f); flat_next_value(f); return; } @@ -1196,6 +1311,33 @@ static void flat_step_k(void *self, ScrDyn *v) { scr_dyn_release(v); } +/* Await(innerResult) for an async delegate: done ends it, else its value + * is yielded as it is. */ +static void flat_iter_k(void *self, ScrDyn *v) { + OpFlat *f = self; + if (!v) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + bool done = false; + ScrDyn *value = flat_result(v, &done); + scr_dyn_release(v); + if (!value) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (done) { + scr_dyn_release(value); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + gen_yield_now(&f->g, value); + scr_dyn_release(value); +} + /* The async-from-sync continuation: PromiseResolve(value) then the result * promise settles; yield* awaits that promise. */ typedef struct { @@ -1220,8 +1362,56 @@ static void op_flat_wrap_k(void *self, ScrDyn *v) { scr_dyn_release(v); } +/* The awaited resume of an async compiled generator delegate, read to its + * value or the done marker: done ends it, else the value is yielded. */ +static void flat_gen_k(void *self, ScrDyn *v) { + OpFlat *f = self; + if (!v) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done) { + scr_dyn_release(v); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + gen_yield_now(&f->g, v); + scr_dyn_release(v); +} + static void flat_delegate_step(OpFlat *f) { - ScrDyn *e = flat_take_element(f); + if (f->iter_kind == FLAT_GEN_ASYNC) { + ScrPromise *p = ((FromAsyncFn)f->gnext->fn)(f->gnext, scr_gen_retain(f->gen)); + op_await_gen(p, f->gread, &flat_gen_k, f); + scr_promise_release(p); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + ScrDyn *r = flat_call(f->iter, "next", NULL, 0); + if (!r) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + op_await_value(r, &flat_iter_k, f); + scr_dyn_release(r); + return; + } + if (f->iter_kind == FLAT_STREAM) { + ScrPromise *p = scr_stream_next_result_dyn(f->stream); + op_await(p, &flat_iter_k, f); + scr_promise_release(p); + return; + } + bool failed = false; + ScrDyn *e = flat_take_element(f, &failed); + if (failed) { + flat_drop_delegate(f); + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } OpFlatWrap *w = op_alloc(sizeof *w); w->base.rc = 1; w->base.drop = &op_flat_wrap_drop; @@ -1247,15 +1437,63 @@ static void flat_val_k(void *self, ScrDyn *v) { gen_complete(&f->g, NULL); return; } - if (v->kind != SCR_DYN_ARR && v->kind != SCR_DYN_STR) { - scr_dyn_release(v); - ScrCaught *c = op_take_error( - op_unsupported("flatMap callback results other than arrays or strings")); - gen_close_source(&f->g, &f->values, c); + if (v->kind == SCR_DYN_ARR || v->kind == SCR_DYN_STR) { + f->inner = v; + f->idx = 0; + flat_delegate_step(f); return; } - f->inner = v; - f->idx = 0; + ScrStream *stream = scr_stream_native_readable_of_dyn(v); + if (stream) { + scr_dyn_release(v); + f->stream = stream; + f->iter_kind = FLAT_STREAM; + flat_delegate_step(f); + return; + } + /* a compiled generator of the callback's declared type: stepped through + * the compiler's helpers */ + if (f->gnext && v->kind == SCR_DYN_TYPED_REF && v->v.typed_ref.type_key && + !strncmp(v->v.typed_ref.type_key, f->gen_async ? "async-generator<" : "generator<", + f->gen_async ? 16 : 10)) { + f->gen = scr_dyn_typed_ref_unbox(v); + scr_dyn_release(v); + f->iter_kind = f->gen_async ? FLAT_GEN_ASYNC : FLAT_GEN_SYNC; + flat_delegate_step(f); + return; + } + /* yield* val: GetIterator(val, async); a compiled generator is its own + * iterator */ + int kind = 0; + ScrDyn *iter = NULL; + if (v->kind == SCR_DYN_TYPED_REF && v->v.typed_ref.type_key && + (!strncmp(v->v.typed_ref.type_key, "async-generator<", 16) || + !strncmp(v->v.typed_ref.type_key, "generator<", 10))) { + kind = v->v.typed_ref.type_key[0] == 'a' ? 2 : 1; + iter = scr_dyn_retain(v); + } else { + iter = scr_dyn_open_iterator(v, &kind, true); + } + if (!iter && kind == 0 && !scr_exc_pending()) { + /* V8's GetIterator over a value without either method */ + if (v->kind == SCR_DYN_NULL || v->kind == SCR_DYN_UNDEF) { + char m[96]; + int n = snprintf(m, sizeof m, + "Cannot read properties of %s (reading 'Symbol(Symbol.asyncIterator)')", + v->kind == SCR_DYN_NULL ? "null" : "undefined"); + scr_throw_error_msg(SCR_ERR_TYPE, m, (size_t)n); + } else { + static const char m[] = "undefined is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, m, sizeof m - 1); + } + } + scr_dyn_release(v); + if (!iter) { + gen_close_source(&f->g, &f->values, scr_exc_take()); + return; + } + f->iter = iter; + f->iter_kind = kind == 2 ? FLAT_ASYNC : FLAT_SYNC; flat_delegate_step(f); } @@ -1300,19 +1538,85 @@ static void op_flat_ret_k(void *self, ScrDyn *v) { op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); return; } - if (r->f->inner) { - scr_dyn_release(r->f->inner); - r->f->inner = NULL; - } + flat_drop_delegate(r->f); gen_close_source(&r->f->g, &r->f->values, NULL); } +/* Whether the delegate's iterator has a callable `name` method. */ +static bool flat_iter_has(OpFlat *f, const char *name) { + if (!f->iter) return false; + ScrDyn *m = flat_get(f->iter, name); + if (!m) { + scr_exc_clear(); + return false; + } + bool callable = scr_dyn_is_callable(m); + scr_dyn_release(m); + return callable; +} + static void flat_ret(OpGen *g, ScrDyn *v) { (void)v; + OpFlat *f = (OpFlat *)g; OpFlatRet *r = op_alloc(sizeof *r); r->base.rc = 1; r->base.drop = &op_flat_ret_drop; - r->f = op_ref((OpFlat *)g); + r->f = op_ref(f); + if (f->iter_kind == FLAT_GEN_ASYNC) { + /* the generator's return(): awaited, then the value */ + ScrPromise *p = ((FromAsyncFn)f->gret->fn)(f->gret, scr_gen_retain(f->gen)); + op_await_gen(p, f->gread, &op_flat_ret_k, r); + scr_promise_release(p); + op_unref(r); + return; + } + if (f->iter_kind == FLAT_GEN_SYNC) { + ScrDyn *res = ((FromSyncFn)f->gret->fn)(f->gret, scr_gen_retain(f->gen)); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + scr_dyn_release(res); + op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); + op_unref(r); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + /* yield*'s return: Await(iterator.return(value)), then the value */ + if (flat_iter_has(f, "return")) { + ScrDyn *arg = scr_dyn_undefined(); + ScrDyn *res = flat_call(f->iter, "return", &arg, 1); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + op_await_value(res, &op_flat_ret_k, r); + scr_dyn_release(res); + } else { + r->stage = 1; + op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); + } + op_unref(r); + return; + } + if (f->iter_kind == FLAT_SYNC && flat_iter_has(f, "return")) { + /* the async-from-sync iterator's return() calls the sync one */ + ScrDyn *arg = scr_dyn_undefined(); + ScrDyn *res = flat_call(f->iter, "return", &arg, 1); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + op_unref(r); + return; + } + scr_dyn_release(res); + } else if (f->iter_kind == FLAT_STREAM) { + scr_stream_release(scr_stream_iterator_close(f->stream)); + } op_await_value(scr_dyn_undefined(), &op_flat_ret_k, r); op_unref(r); } @@ -1321,12 +1625,72 @@ static void flat_ret(OpGen *g, ScrDyn *v) { * yield* rejects with Node's TypeError, which leaves the for await and * closes map's generator. */ static void flat_thr(OpGen *g, ScrCaught *c) { - (void)c; OpFlat *f = (OpFlat *)g; - if (f->inner) { - scr_dyn_release(f->inner); - f->inner = NULL; + if (f->iter_kind == FLAT_GEN_ASYNC || f->iter_kind == FLAT_GEN_SYNC) { + /* the generator's throw(error) answers the next step */ + ScrDyn *err = scr_caught_to_dyn(c); + if (f->iter_kind == FLAT_GEN_ASYNC) { + ScrPromise *p = ((FromAsyncThrowFn)f->gthr->fn)(f->gthr, scr_gen_retain(f->gen), err); + op_await_gen(p, f->gread, &flat_gen_k, f); + scr_promise_release(p); + return; + } + ScrDyn *v = ((FromSyncThrowFn)f->gthr->fn)(f->gthr, scr_gen_retain(f->gen), err); + if (!v) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + return; + } + if (v == op_k_done) { + scr_dyn_release(v); + flat_drop_delegate(f); + flat_next_value(f); + return; + } + op_await_value(v, &flat_step_k, f); + scr_dyn_release(v); + return; } + if ((f->iter_kind == FLAT_ASYNC || f->iter_kind == FLAT_SYNC) && flat_iter_has(f, "throw")) { + /* yield*'s throw: the delegate's throw(error) answers the next step */ + ScrDyn *err = scr_caught_to_dyn(c); + ScrDyn *res = flat_call(f->iter, "throw", &err, 1); + scr_dyn_release(err); + if (!res) { + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_exc_take()); + return; + } + if (f->iter_kind == FLAT_ASYNC) { + op_await_value(res, &flat_iter_k, f); + scr_dyn_release(res); + return; + } + ScrPromise *p = op_resolved(res); + scr_dyn_release(res); + op_await(p, &flat_iter_k, f); + scr_promise_release(p); + return; + } + if (f->iter_kind == FLAT_STREAM) { + /* the stream's iterator rethrows after destroying the stream */ + ScrDyn *err = scr_caught_to_dyn(c); + ScrError *e = err ? scr_error_from_dyn(err) : NULL; + if (scr_exc_pending()) scr_exc_clear(); + if (err) scr_dyn_release(err); + scr_stream_release(scr_stream_destroy(f->stream, e)); + if (e) scr_error_release(e); + flat_drop_delegate(f); + gen_close_source(g, &f->values, scr_caught_retain(c)); + return; + } + if (f->iter_kind != FLAT_NONE && flat_iter_has(f, "return")) { + /* no throw method: the iterator closes first */ + ScrDyn *res = flat_call(f->iter, "return", NULL, 0); + if (res) scr_dyn_release(res); + else scr_exc_clear(); + } + flat_drop_delegate(f); ScrError *e = op_coded_error(SCR_ERR_TYPE, NULL, "The iterator does not provide a 'throw' method."); gen_close_source(g, &f->values, op_take_error(e)); } @@ -1370,12 +1734,6 @@ static void op_from_drop(OpObj *o) { /* The helper ABIs: the closure first, then the declared parameters, which * the callee owns. */ -typedef ScrDyn *(*FromSyncFn)(ScrClosure *, ScrGen *); -typedef ScrDyn *(*FromSyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); -typedef ScrPromise *(*FromAsyncFn)(ScrClosure *, ScrGen *); -typedef ScrPromise *(*FromAsyncThrowFn)(ScrClosure *, ScrGen *, ScrDyn *); -typedef ScrDyn *(*FromReadFn)(ScrClosure *, void *); - /* `await p` over an async generator's resume promise: the IteratorResult * record goes through `read` (the value, or the done marker) before k. * Awaiting the typed promise itself keeps from.js's single await. */ @@ -2052,6 +2410,7 @@ struct OpChan { ScrPromise *resolve; /* +1: the deferred a write or the final settles, NULL once settled */ int settled; /* CHAN_*: what settled the deferred, for ack() */ bool aborted; /* destroy ran (fromAsyncGen's ac.abort()) */ + ScrClosure *abort; /* +1: the body's AbortController's abort(), or NULL */ /* An async function body: its promise's then() (duplexify.js) settles * `promise` there; the final waits for it. */ bool finalized; /* final() has started */ @@ -2066,6 +2425,7 @@ static SCR_TL OpChan *op_chan_live = NULL; static void op_chan_drop(OpObj *o) { OpChan *c = (OpChan *)o; if (c->body_err) scr_error_release(c->body_err); + if (c->abort) scr_closure_release(c->abort); if (c->d) scr_stream_release(c->d); if (c->promise) scr_promise_release(c->promise); if (c->resolve) scr_promise_release(c->resolve); @@ -2091,6 +2451,8 @@ static void op_chan_retire(OpChan *c) { c->live_prev = c->live_next = NULL; ScrStream *d = c->d; c->d = NULL; + if (c->abort) scr_closure_release(c->abort); + c->abort = NULL; if (c->promise) scr_promise_release(c->promise); if (c->resolve) scr_promise_release(c->resolve); c->promise = c->resolve = NULL; @@ -2233,6 +2595,21 @@ static void op_chan_final_inv(ScrClosure *cb, ScrStream *s) { op_unref(c); } +/* fromAsyncGen's ac.abort(err): the body's signal fires its abort + * listeners once (a body that declares the options argument); without an + * error the reason is null (destroy passes `err || null`). */ +static void op_chan_abort(OpChan *c, ScrError *err) { + c->aborted = true; + ScrClosure *abort = c->abort; + if (!abort) return; + c->abort = NULL; + ScrDyn *reason = err ? scr_dyn_from_error(err) : NULL; + if (!reason) reason = scr_dyn_new_null(); + ((void (*)(ScrClosure *, ScrDyn *))abort->fn)(abort, reason); /* reason moves in */ + if (scr_exc_pending()) scr_exc_clear(); + scr_closure_release(abort); +} + /* destroy: fromAsyncGen's (ac.abort(); a source waiting for a write or * the final is unblocked with the done marker and a no-op callback; * cb(err)), then from()'s close of the function's iterator. caps: the @@ -2240,7 +2617,7 @@ static void op_chan_final_inv(ScrClosure *cb, ScrStream *s) { static void op_dup_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { OpChan *c = scr_box_get_ref(cb->caps[0]); ScrClosure *from = scr_box_get_ref(cb->caps[1]); - c->aborted = true; + op_chan_abort(c, err); if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); op_chan_retire(c); op_unref(c); @@ -2250,14 +2627,11 @@ static void op_dup_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { typedef ScrGen *(*OpSourceFn)(ScrClosure *, ScrClosure *, ScrClosure *); typedef ScrGen *(*OpStageFn)(ScrClosure *, ScrGen *); +typedef ScrGen *(*OpStageOptsFn)(ScrClosure *, ScrGen *, ScrDyn *); -/* Duplex.from(fn) for an async generator function: the compiled stage - * (borrowed) takes the source generator and answers its own; source - * (borrowed) makes the source generator from next() and ack(); the - * compiler's helpers over the stage's generator are borrowed. +1, or - * NULL with the function's throw pending. */ -ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, - ScrClosure *read, ScrClosure *ret, ScrClosure *thr) { +/* A new channel; with a signal (borrowed), the body's AbortController's + * abort() (borrowed) is kept for destroy and *opts is { signal } (+1). */ +static OpChan *op_chan_new(ScrClosure *abort, ScrDyn *signal, ScrDyn **opts) { op_sentinels_init(); static SCR_TL bool registered = false; if (!registered) { @@ -2267,10 +2641,35 @@ ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, Scr OpChan *c = op_alloc(sizeof *c); c->base.rc = 1; c->base.drop = &op_chan_drop; + *opts = NULL; + if (signal) { + c->abort = scr_closure_retain(abort); + *opts = scr_dyn_new_obj(); + scr_dyn_obj_set(*opts, "signal", 6, scr_dyn_retain(signal)); + } op_chan_defer(c); + return c; +} + +/* Duplex.from(fn) for an async generator function: the compiled stage + * (borrowed) takes the source generator and answers its own; source + * (borrowed) makes the source generator from next() and ack(); the + * compiler's helpers over the stage's generator are borrowed. With a + * signal, the stage also takes { signal }. +1, or NULL with the + * function's throw pending. */ +static ScrStream *op_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, ScrDyn *signal) { + ScrDyn *opts; + OpChan *c = op_chan_new(abort, signal, &opts); ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), op_chan_closure((void *)&op_chan_ack_inv, c)); - ScrGen *g = scr_exc_pending() ? NULL : ((OpStageFn)stage->fn)(stage, src); /* src moves in */ + ScrGen *g = NULL; /* src and opts move in */ + if (scr_exc_pending()) { + if (opts) scr_dyn_release(opts); + } else { + g = opts ? ((OpStageOptsFn)stage->fn)(stage, src, opts) : ((OpStageFn)stage->fn)(stage, src); + } if (scr_exc_pending()) { if (g) scr_gen_release(g); op_unref(c); @@ -2307,6 +2706,17 @@ ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, Scr return d; } +ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr) { + return op_duplex_from_gen(stage, source, next, read, ret, thr, NULL, NULL); +} + +ScrStream *scr_stream_duplex_from_gen_signal(ScrClosure *stage, ScrClosure *source, ScrClosure *next, + ScrClosure *read, ScrClosure *ret, ScrClosure *thr, + ScrClosure *abort, ScrDyn *signal) { + return op_duplex_from_gen(stage, source, next, read, ret, thr, abort, signal); +} + /* ── Duplex.from(asyncFunction) ── duplexify.js: the function's promise * must settle to a nullish value (else ERR_INVALID_RETURN_VALUE when the * final awaits it), a return before the input was consumed destroys the @@ -2314,6 +2724,7 @@ ScrStream *scr_stream_duplex_from_gen(ScrClosure *stage, ScrClosure *source, Scr * writable only, its final waiting for the promise. */ typedef ScrPromise *(*OpAsyncStageFn)(ScrClosure *, ScrGen *); +typedef ScrPromise *(*OpAsyncStageOptsFn)(ScrClosure *, ScrGen *, ScrDyn *); typedef ScrDyn *(*OpBoxFn)(ScrClosure *, ScrPromise *); typedef struct { @@ -2376,7 +2787,7 @@ static void op_chan_final_async_inv(ScrClosure *cb, ScrStream *s) { /* fromAsyncGen's destroy alone (no from() iterator to close) */ static void op_chan_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { OpChan *c = op_chan_of(cb); - c->aborted = true; + op_chan_abort(c, err); if (c->resolve) op_chan_settle(c, scr_stream_from_done(), CHAN_ABORT); op_chan_retire(c); op_unref(c); @@ -2384,23 +2795,22 @@ static void op_chan_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { } /* Duplex.from(fn) for an async function: the compiled stage (borrowed) - * takes the source generator and answers its promise; source and box - * (promise → dyn promise) borrowed. +1, or NULL with the function's throw - * pending. */ -ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box) { - op_sentinels_init(); - static SCR_TL bool registered = false; - if (!registered) { - registered = true; - scr_atexit(op_chan_atexit); - } - OpChan *c = op_alloc(sizeof *c); - c->base.rc = 1; - c->base.drop = &op_chan_drop; - op_chan_defer(c); + * takes the source generator (and, with a signal, { signal }) and answers + * its promise; source and box (promise → dyn promise) borrowed. +1, or + * NULL with the function's throw pending. */ +static ScrStream *op_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box, + ScrClosure *abort, ScrDyn *signal) { + ScrDyn *opts; + OpChan *c = op_chan_new(abort, signal, &opts); ScrGen *src = ((OpSourceFn)source->fn)(source, op_chan_closure((void *)&op_chan_next_inv, c), op_chan_closure((void *)&op_chan_ack_inv, c)); - ScrPromise *p = scr_exc_pending() ? NULL : ((OpAsyncStageFn)stage->fn)(stage, src); + ScrPromise *p = NULL; /* src and opts move in */ + if (scr_exc_pending()) { + if (opts) scr_dyn_release(opts); + } else { + p = opts ? ((OpAsyncStageOptsFn)stage->fn)(stage, src, opts) + : ((OpAsyncStageFn)stage->fn)(stage, src); + } if (scr_exc_pending()) { if (p) scr_promise_release(p); op_unref(c); @@ -2427,6 +2837,16 @@ ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source return d; } +ScrStream *scr_stream_duplex_from_async_fn(ScrClosure *stage, ScrClosure *source, ScrClosure *box) { + return op_duplex_from_async_fn(stage, source, box, NULL, NULL); +} + +ScrStream *scr_stream_duplex_from_async_fn_signal(ScrClosure *stage, ScrClosure *source, + ScrClosure *box, ScrClosure *abort, + ScrDyn *signal) { + return op_duplex_from_async_fn(stage, source, box, abort, signal); +} + /* ── the promise operators ─────────────────────────────────────────── */ enum { PK_DYN, PK_F64, PK_BOOL, PK_STR, PK_VOID }; @@ -2439,6 +2859,8 @@ typedef struct { ScrDyn *fn; /* +1 or NULL */ ScrDyn *signal; /* +1 or NULL */ ScrDyn *acc; /* toArray's array / reduce's accumulator (+1) */ + ScrClosure *ctl; /* +1: reduce's ac.abort(), run when it ends, or NULL */ + ScrDyn *cb_opts; /* +1: the { signal } the reducer takes as its third argument, or NULL */ int op; int kind; /* the promise payload (PK_*) */ bool has_init, got_any, negate; @@ -2453,9 +2875,21 @@ static void op_fn_drop(OpObj *o) { if (a->fn) scr_dyn_release(a->fn); if (a->signal) scr_dyn_release(a->signal); if (a->acc) scr_dyn_release(a->acc); + if (a->ctl) scr_closure_release(a->ctl); + if (a->cb_opts) scr_dyn_release(a->cb_opts); free(a); } +/* reduce's `finally { ac.abort(); }`, before its promise settles. */ +static void op_fn_abort_ctl(OpFn *a) { + if (!a->ctl) return; + ScrClosure *ctl = a->ctl; + a->ctl = NULL; + ((void (*)(ScrClosure *))ctl->fn)(ctl); + if (scr_exc_pending()) scr_exc_clear(); + scr_closure_release(ctl); +} + static OpFn *op_fn_new(int op, int kind) { OpFn *a = op_alloc(sizeof *a); a->base.rc = 1; @@ -2469,6 +2903,7 @@ static OpFn *op_fn_new(int op, int kind) { /* Settles the operator's promise with v converted to its payload kind (a * value of the wrong kind rejects with the checked conversion's error). */ static void op_fn_resolve(OpFn *a, ScrDyn *v /*borrowed*/) { + op_fn_abort_ctl(a); switch (a->kind) { case PK_VOID: scr_promise_fulfill_void(a->p); @@ -2493,7 +2928,10 @@ static void op_fn_resolve(OpFn *a, ScrDyn *v /*borrowed*/) { scr_promise_reject_pending(a->p); } -static void op_fn_reject(OpFn *a, const ScrCaught *c) { op_reject(a->p, c); } +static void op_fn_reject(OpFn *a, const ScrCaught *c) { + op_fn_abort_ctl(a); + op_reject(a->p, c); +} static void op_fn_next(OpFn *a); @@ -2634,8 +3072,8 @@ static void op_fn_val_k(void *self, ScrDyn *v) { op_fn_next(a); return; } - ScrDyn *args[2] = {a->acc, v}; - ScrDyn *r = scr_dyn_call(a->fn, args, 2, "reducer"); + ScrDyn *args[3] = {a->acc, v, a->cb_opts}; + ScrDyn *r = scr_dyn_call(a->fn, args, a->cb_opts ? 3 : 2, "reducer"); scr_dyn_release(v); if (!r) { op_fn_close(a, scr_exc_take(), NULL); @@ -2717,7 +3155,7 @@ static bool op_map_options(const ScrDyn *options, double *concurrency, double *h /* map(fn, options) with fn already checked: +1 generator, or NULL with * the error pending. */ -static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode) { +static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode, ScrDyn *cb_opts) { op_sentinels_init(); if (!op_check_options(options)) return NULL; bool ok; @@ -2728,26 +3166,28 @@ static OpMap *op_map_make(ScrStream *s, ScrDyn *fn, ScrDyn *options, int mode) { if (signal) scr_dyn_release(signal); return NULL; } - OpMap *m = map_new(s, fn, mode, concurrency, hwm, signal); + OpMap *m = map_new(s, fn, mode, concurrency, hwm, signal, cb_opts); if (signal) scr_dyn_release(signal); return m; } -ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrStream *scr_stream_op_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_MAP); + OpMap *m = op_map_make(s, fn, options, MODE_MAP, cb_opts); return m ? op_from_gen(&m->g) : NULL; } -ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrStream *scr_stream_op_filter(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_FILTER); + OpMap *m = op_map_make(s, fn, options, MODE_FILTER, cb_opts); return m ? op_from_gen(&m->g) : NULL; } -ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +static ScrStream *op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts, + ScrClosure *next, ScrClosure *read, ScrClosure *ret, + ScrClosure *thr, bool async_gen) { if (!op_check_fn(fn, "fn", 1)) return NULL; - OpMap *m = op_map_make(s, fn, options, MODE_MAP); + OpMap *m = op_map_make(s, fn, options, MODE_MAP, cb_opts); if (!m) return NULL; OpFlat *f = op_alloc(sizeof *f); gen_init(&f->g, &op_flat_body_drop); @@ -2755,9 +3195,34 @@ ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options) { f->g.ret = &flat_ret; f->g.thr = &flat_thr; f->values.g = &m->g; /* moves */ + if (next) { + f->gnext = scr_closure_retain(next); + f->gread = read ? scr_closure_retain(read) : NULL; + f->gret = scr_closure_retain(ret); + f->gthr = scr_closure_retain(thr); + f->gen_async = async_gen; + } return op_from_gen(&f->g); } +ScrStream *scr_stream_op_flat_map(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_flat_map(s, fn, options, cb_opts, NULL, NULL, NULL, NULL, false); +} + +/* flatMap whose callback returns a compiled generator: the helpers over + * its type (borrowed) step the generators it yields from. */ +ScrStream *scr_stream_op_flat_map_async_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *read, + ScrClosure *ret, ScrClosure *thr) { + return op_flat_map(s, fn, options, cb_opts, next, read, ret, thr, true); +} + +ScrStream *scr_stream_op_flat_map_sync_gen(ScrStream *s, ScrDyn *fn, ScrDyn *options, + ScrDyn *cb_opts, ScrClosure *next, ScrClosure *ret, + ScrClosure *thr) { + return op_flat_map(s, fn, options, cb_opts, next, NULL, ret, thr, false); +} + /* drop(number, options) / take(number, options) */ static ScrStream *op_slice(ScrStream *s, ScrDyn *number, ScrDyn *options, bool take) { op_sentinels_init(); @@ -2842,8 +3307,8 @@ static void op_noop_listener(ScrClosure *cb, void *err) { (void)err; } -ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, - ScrDyn *options, double kind) { +static ScrPromise *op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *ctl, ScrDyn *cb_opts) { op_sentinels_init(); OpFn *a = op_fn_new(FN_REDUCE, (int)kind); if (!op_check_fn(fn, "reducer", 2)) return op_fn_reject_now(a); @@ -2859,6 +3324,7 @@ ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool ha ScrDyn *reason = op_get(a->signal, "reason"); ScrError *e = op_abort_error(reason ? reason : scr_dyn_undefined()); if (reason) scr_dyn_release(reason); + /* (thrown before the controller exists) */ /* this.once('error', () => {}) */ ScrStr *name = scr_str_new("error", 5); scr_emitter_release(scr_emitter_on((ScrEmitter *)s, name, @@ -2874,40 +3340,97 @@ ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool ha op_unref(a); return p; } + /* const ac = new AbortController() (options.signal's listener that + * aborts it too is registered by the caller) */ + if (ctl) { + a->ctl = scr_closure_retain(ctl); + a->cb_opts = cb_opts && cb_opts->kind != SCR_DYN_UNDEF ? scr_dyn_retain(cb_opts) : NULL; + } return op_fn_start(a); } +ScrPromise *scr_stream_op_reduce(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind) { + return op_reduce(s, fn, init, has_init, options, kind, NULL, NULL); +} + +/* reduce with a reducer that reads its { signal }: abort is ac.abort() + * (borrowed), cb_opts `{ signal: ac.signal }`. */ +ScrPromise *scr_stream_op_reduce_signal(ScrStream *s, ScrDyn *fn, ScrDyn *init, bool has_init, + ScrDyn *options, double kind, ScrClosure *abort, + ScrDyn *cb_opts) { + return op_reduce(s, fn, init, has_init, options, kind, abort, cb_opts); +} + +/* `options?.signal`: +1, undefined when absent. */ +ScrDyn *scr_stream_op_signal_of(ScrDyn *options) { + if (!options || options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) + return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *sig = op_get(options, "signal"); + if (scr_exc_pending()) scr_exc_clear(); + return sig ? sig : scr_dyn_retain(scr_dyn_undefined()); +} + +/* `{ once: true }`: +1. */ +ScrDyn *scr_stream_op_once_options(void) { + ScrDyn *o = scr_dyn_new_obj(); + scr_dyn_obj_set(o, "once", 4, scr_dyn_new_bool(true)); + return o; +} + +/* `[options?.signal].filter(Boolean)` for AbortSignal.any: +1 array. */ +ScrDyn *scr_stream_op_signal_list(ScrDyn *options) { + ScrDyn *list = scr_dyn_new_arr(); + if (options && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL) { + /* A signal that fails validateAbortSignal is left out: the operator + * itself throws Node's ERR_INVALID_ARG_TYPE for it. */ + bool ok; + ScrDyn *sig = op_signal(options, &ok); + if (sig) scr_dyn_arr_push(list, sig); /* moves */ + if (scr_exc_pending()) scr_exc_clear(); + } + return list; +} + +/* `{ signal }`, the callbacks' second argument: +1 object (borrows signal). */ +ScrDyn *scr_stream_op_signal_opts(ScrDyn *signal) { + ScrDyn *o = scr_dyn_new_obj(); + scr_dyn_obj_set(o, "signal", 6, scr_dyn_retain(signal)); + return o; +} + /* some/find/every/forEach: the loop over map's generator (filter's for * some, find and every). */ -static ScrPromise *op_gen_fn(ScrStream *s, ScrDyn *fn, ScrDyn *options, int op, int mode, int kind) { +static ScrPromise *op_gen_fn(ScrStream *s, ScrDyn *fn, ScrDyn *options, int op, int mode, int kind, + ScrDyn *cb_opts) { op_sentinels_init(); OpFn *a = op_fn_new(op, kind); if (!op_check_fn(fn, "fn", 1)) return op_fn_reject_now(a); - OpMap *m = op_map_make(s, fn, options, mode); + OpMap *m = op_map_make(s, fn, options, mode, cb_opts); if (!m) return op_fn_reject_now(a); a->src.g = &m->g; /* moves */ return op_fn_start(a); } -ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_SOME, MODE_FILTER, PK_BOOL); +ScrPromise *scr_stream_op_some(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_SOME, MODE_FILTER, PK_BOOL, cb_opts); } -ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_FIND, MODE_FILTER, PK_DYN); +ScrPromise *scr_stream_op_find(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_FIND, MODE_FILTER, PK_DYN, cb_opts); } -ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options) { - return op_gen_fn(s, fn, options, FN_FOR_EACH, MODE_FOREACH, PK_VOID); +ScrPromise *scr_stream_op_for_each(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { + return op_gen_fn(s, fn, options, FN_FOR_EACH, MODE_FOREACH, PK_VOID, cb_opts); } /* every(fn, options) = !(await some.call(this, async (...args) => * !(await fn(...args)), options)) */ -ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options) { +ScrPromise *scr_stream_op_every(ScrStream *s, ScrDyn *fn, ScrDyn *options, ScrDyn *cb_opts) { op_sentinels_init(); OpFn *outer = op_fn_new(FN_EVERY, PK_BOOL); if (!op_check_fn(fn, "fn", 1)) return op_fn_reject_now(outer); - ScrPromise *some = op_gen_fn(s, fn, options, FN_EVERY, MODE_EVERY, PK_DYN); + ScrPromise *some = op_gen_fn(s, fn, options, FN_EVERY, MODE_EVERY, PK_DYN, cb_opts); ScrPromise *p = scr_promise_retain(outer->p); op_await(some, &op_every_k, outer); scr_promise_release(some); diff --git a/tests/corpus/7433-readable-operator-signal.mjs b/tests/corpus/7433-readable-operator-signal.mjs new file mode 100644 index 000000000..38bfbf367 --- /dev/null +++ b/tests/corpus/7433-readable-operator-signal.mjs @@ -0,0 +1,36 @@ +// The Readable operators pass their callbacks { signal } as the second +// argument; map and filter's signal aborts when the options' signal +// does, reduce's signal is aborted once reduce settles, and map may +// return any value. +import { Readable } from "node:stream"; +const log = []; +log.push(JSON.stringify(await Readable.from([1, 2, 3]).map((n) => ({ n, sq: n * n })).toArray())); +log.push(JSON.stringify(await Readable.from(["a", "bb"]).map((s) => s.length).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2]).map((n, { signal }) => `${n}:${signal.aborted}`).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2, 3, 4]).filter(async (n, opts) => n % 2 === 0 && !opts.signal.aborted).toArray())); +log.push(String(await Readable.from([1, 2, 3]).reduce((acc, n, { signal }) => acc + n + (signal.aborted ? 100 : 0), 0))); +let kept; +await Readable.from([1]).reduce((acc, n, { signal }) => { kept = signal; return acc + n; }, 0); +log.push(`reduce signal after ${kept.aborted}`); +let mapKept; +await Readable.from([1]).map((n, { signal }) => { mapKept = signal; return n; }).toArray(); +log.push(`map signal after ${mapKept.aborted}`); +const ac = new AbortController(); +const events = []; +const p = Readable.from([1, 2, 3]).map(async (n, { signal }) => { + signal.addEventListener("abort", () => events.push(`abort ${n}`)); + await new Promise((r) => setTimeout(r, 10)); + return n; +}, { signal: ac.signal }).toArray(); +setTimeout(() => ac.abort(), 5); +try { await p; } catch (e) { log.push(`map aborted ${e.name}`); } +log.push(events.join(",")); +const dyn = [Readable.from([4, 5])][0]; +log.push(JSON.stringify(await dyn.map((n, { signal }) => n + Number(signal.aborted)).toArray())); +let seen = 0; +await Readable.from([5, 6]).forEach((n, { signal }) => { if (signal) seen++; }); +log.push(`forEach ${seen}`); +log.push(String(await Readable.from([1, 2, 3]).some((n, { signal }) => n > 2 && !signal.aborted))); +log.push(String(await Readable.from([1, 2, 3]).find((n, { signal }) => n > 1 && !signal.aborted))); +log.push(String(await Readable.from([1, 2, 3]).every((n, { signal }) => n > 0 && !signal.aborted))); +console.log(log.join("\n")); diff --git a/tests/corpus/7434-readable-operator-values.ts b/tests/corpus/7434-readable-operator-values.ts new file mode 100644 index 000000000..49f7a0fa9 --- /dev/null +++ b/tests/corpus/7434-readable-operator-values.ts @@ -0,0 +1,32 @@ +// Typed Readable operators: map returns numbers and records, flatMap +// takes generator functions and sets, the callbacks may declare the +// { signal } argument, and read() on an object-mode stream answers the +// values pushed. +import { Readable } from "node:stream"; +async function* pair(n: number): AsyncGenerator { + yield n; + yield n * 100; +} +function* letters(s: string): Generator { + for (const c of s) yield c; +} +async function main(): Promise { + const squares = await Readable.from([1, 2, 3]).map((n: number) => n * n).toArray(); + const records = await Readable.from(["a", "bb"]).map((s: string) => ({ s, len: s.length })).toArray(); + const pairs = await Readable.from([1, 2]).flatMap(pair).toArray(); + const chars = await Readable.from(["hi"]).flatMap(letters).toArray(); + const sets = await Readable.from([7]).flatMap((n: number) => new Set([n, n + 1])).toArray(); + const signals = await Readable.from([1]).map((n: number, opts?: { signal?: AbortSignal }) => `${n}:${opts?.signal?.aborted}`).toArray(); + const total = await Readable.from([1, 2]).reduce((acc: number, n: number, opts?: { signal?: AbortSignal }) => acc + n + (opts?.signal?.aborted ? 1 : 0), 0); + console.log(JSON.stringify({ squares, records, pairs, chars, sets, signals, total })); + const r = new Readable({ objectMode: true, read(): void {} }); + r.push({ a: 1 }); + r.push(5); + r.push(null); + r.once("readable", () => { + const first: unknown = r.read(); + const second: unknown = r.read(); + console.log(JSON.stringify(first), second, r.read()); + }); +} +void main(); diff --git a/tests/corpus/7435-readable-flatmap-iterables.mjs b/tests/corpus/7435-readable-flatmap-iterables.mjs new file mode 100644 index 000000000..f0ee79b5a --- /dev/null +++ b/tests/corpus/7435-readable-flatmap-iterables.mjs @@ -0,0 +1,29 @@ +// flatMap over what the callback returns: async and sync generators, a +// Set, a Map, a Readable, and a value that is not iterable (V8's +// TypeError); the generator's steps interleave with process.nextTick as +// in Node, breaking out of the loop runs the inner generator's finally, +// and an inner throw rejects toArray(). +import { Readable } from "node:stream"; +const log = []; +log.push(JSON.stringify(await Readable.from(["x"]).flatMap(async function* (s) { yield s; yield s + s; }).toArray())); +log.push(JSON.stringify(await Readable.from([2]).flatMap(function* (n) { yield n; yield -n; }).toArray())); +log.push(JSON.stringify(await Readable.from([1, 2]).flatMap((n) => new Set([n, n])).toArray())); +log.push(JSON.stringify(await Readable.from(["p"]).flatMap((s) => Readable.from([s, s.toUpperCase()])).toArray())); +log.push(JSON.stringify(await Readable.from([3]).flatMap((n) => new Map([[n, "v"]])).toArray())); +log.push(JSON.stringify(await Readable.from([{ k: 1 }]).flatMap((o) => [o, { k: o.k + 1 }]).toArray())); +try { await Readable.from([1]).flatMap(() => 5).toArray(); } catch (e) { log.push(`${e.name} ${e.message}`); } +try { await Readable.from([1]).flatMap(() => null).toArray(); } catch (e) { log.push(`${e.name} ${e.message}`); } +const order = []; +let n = 0; +const tick = () => { order.push(`t${n}`); if (++n < 5) process.nextTick(tick); }; +process.nextTick(tick); +for await (const v of Readable.from([1]).flatMap(async function* (k) { order.push("gen start"); yield k; order.push("gen mid"); yield k + 1; })) order.push(`v${v}`); +log.push(order.join(" ")); +const closed = []; +const early = Readable.from([1, 2]).flatMap(async function* (k) { try { yield k; yield k * 10; } finally { closed.push(`finally ${k}`); } }); +for await (const v of early) { closed.push(`got ${v}`); break; } +log.push(closed.join(" ")); +const thrown = []; +try { await Readable.from([1]).flatMap(async function* () { yield 1; throw new Error("inner boom"); }).toArray(); } catch (e) { thrown.push(e.message); } +log.push(thrown.join(" ")); +console.log(log.join("\n")); diff --git a/tests/corpus/7436-readable-read-any-value.mjs b/tests/corpus/7436-readable-read-any-value.mjs new file mode 100644 index 000000000..5e19b16c8 --- /dev/null +++ b/tests/corpus/7436-readable-read-any-value.mjs @@ -0,0 +1,17 @@ +// readable.read() answers any value on an object-mode stream, and a +// Buffer or a string (with an encoding) in byte mode. +import { Readable, PassThrough } from "node:stream"; +const r = new Readable({ objectMode: true, read() {} }); +r.push({ a: 1 }); r.push(2); r.push("three"); r.push([4]); r.push(null); +r.once("readable", () => { let v; while ((v = r.read()) !== null) console.log("read", JSON.stringify(v)); }); +const p = new PassThrough({ objectMode: true }); +p.write([1, 2]); +p.end(); +p.once("readable", () => console.log("pt", JSON.stringify(p.read()), p.read())); +const b = new PassThrough(); +b.end("bytes!"); +b.once("readable", () => { const c = b.read(3); console.log("bytes", Buffer.isBuffer(c), String(c), String(b.read())); }); +const e = new PassThrough(); +e.setEncoding("utf8"); +e.end("text"); +e.once("readable", () => { const t = e.read(); console.log("encoded", typeof t, t); }); diff --git a/tests/corpus/7437-duplex-from-signal.mjs b/tests/corpus/7437-duplex-from-signal.mjs new file mode 100644 index 000000000..263225bbb --- /dev/null +++ b/tests/corpus/7437-duplex-from-signal.mjs @@ -0,0 +1,54 @@ +// A Duplex.from body may take { signal } (duplexify.js's fromAsyncGen): +// destroy aborts it with the destroy error, or with an AbortError when +// there is none, and compose passes it to its function stages. +import { Duplex, Readable, compose } from "node:stream"; +import { once } from "node:events"; + +const d = Duplex.from(async function* (source, { signal }) { + console.log("body", typeof signal, signal.aborted); + signal.addEventListener("abort", () => console.log("abort event", signal.aborted, signal.reason)); + for await (const chunk of source) yield String(chunk).toUpperCase(); + console.log("source ended"); +}); +d.on("data", (c) => console.log("data", c)); +d.write("a"); +d.write("b"); +await new Promise((r) => setTimeout(r, 10)); +d.destroy(); +await once(d, "close"); +console.log("closed d"); + +const seen = []; +const fin = Duplex.from(async function (source, { signal }) { + signal.addEventListener("abort", () => console.log("abort", seen.join(","))); + for await (const chunk of source) seen.push(String(chunk)); + console.log("done", signal.aborted); +}); +fin.on("finish", () => console.log("finish")); +fin.write("1"); +fin.write("2"); +fin.end(); +await once(fin, "close"); +console.log("closed fin"); + +const boom = new Error("boom"); +const e = Duplex.from(async function (source, { signal }) { + signal.addEventListener("abort", () => console.log("e abort", String(signal.reason), signal.reason === boom)); + await new Promise((r) => setTimeout(r, 50)); +}); +e.on("error", (err) => console.log("e error", err === boom)); +setTimeout(() => e.destroy(boom), 5); +await new Promise((r) => e.on("close", r)); +console.log("closed e"); + +const c = compose( + async function* (source, { signal }) { + for await (const chunk of source) yield `${chunk}!${signal.aborted}`; + }, + async function* (source, { signal }) { + for await (const chunk of source) yield chunk.toUpperCase(); + }, +); +Readable.from(["a", "b"]).pipe(c); +c.on("data", (x) => console.log("compose", String(x))); +c.on("end", () => console.log("compose end")); diff --git a/tests/corpus/7438-duplex-from-signal.ts b/tests/corpus/7438-duplex-from-signal.ts new file mode 100644 index 000000000..358bd3e68 --- /dev/null +++ b/tests/corpus/7438-duplex-from-signal.ts @@ -0,0 +1,26 @@ +// Typed Duplex.from bodies that declare the options argument: an async +// generator function and an async function. +import { Duplex } from "node:stream"; + +const d = Duplex.from(async function* (source: AsyncIterable, opts: { signal: AbortSignal }) { + opts.signal.addEventListener("abort", () => console.log("aborted", opts.signal.aborted)); + for await (const chunk of source) { + yield `<${chunk}>`; + } +}); +d.on("data", (c: string) => console.log("data", c)); +d.on("end", () => console.log("end")); +d.on("close", () => { + console.log("close"); + const w = Duplex.from(async function (source: AsyncIterable, { signal }: { signal: AbortSignal }): Promise { + let n = 0; + for await (const chunk of source) n += chunk.length; + console.log("total", n, signal.aborted); + }); + w.on("finish", () => console.log("finish")); + w.on("close", () => console.log("w close")); + w.write("abc"); + w.end("de"); +}); +d.write("x"); +d.end("y"); diff --git a/tests/corpus/7439-readable-read-string-chunks.ts b/tests/corpus/7439-readable-read-string-chunks.ts new file mode 100644 index 000000000..2fc5dbed6 --- /dev/null +++ b/tests/corpus/7439-readable-read-string-chunks.ts @@ -0,0 +1,17 @@ +// A typed read() answers the strings a stream holds: the first value a +// lazy Readable.from pull over a parsed array produces, and the decoded +// text once _read calls setEncoding(). +import { Readable } from "node:stream"; +const r = Readable.from(JSON.parse('["j1", "j2"]')); +const chunk = r.read(); +console.log("first", typeof chunk, chunk === null ? "null" : chunk.toString("hex")); +r.once("readable", () => console.log("later", JSON.stringify(r.read()))); +const e = new Readable({ + read() { + this.setEncoding("utf8"); + this.push(Buffer.from("ab")); + this.push(null); + }, +}); +const text = e.read(); +console.log("encoded", typeof text, text === null ? "null" : text.toString("hex")); diff --git a/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json b/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json new file mode 100644 index 000000000..3ef40f751 --- /dev/null +++ b/tests/harness/corpus-costs/7433-readable-operator-signal.mjs.json @@ -0,0 +1 @@ +{ "plain": 3885, "san": 5128 } diff --git a/tests/harness/corpus-costs/7434-readable-operator-values.ts.json b/tests/harness/corpus-costs/7434-readable-operator-values.ts.json new file mode 100644 index 000000000..fdbfd8cde --- /dev/null +++ b/tests/harness/corpus-costs/7434-readable-operator-values.ts.json @@ -0,0 +1 @@ +{ "plain": 2465, "san": 2933 } diff --git a/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json b/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json new file mode 100644 index 000000000..8c31cc905 --- /dev/null +++ b/tests/harness/corpus-costs/7435-readable-flatmap-iterables.mjs.json @@ -0,0 +1 @@ +{ "plain": 2265, "san": 2990 } diff --git a/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json b/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json new file mode 100644 index 000000000..c4a7cbc84 --- /dev/null +++ b/tests/harness/corpus-costs/7436-readable-read-any-value.mjs.json @@ -0,0 +1 @@ +{ "plain": 1675, "san": 2211 } diff --git a/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json b/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json new file mode 100644 index 000000000..dfbf933f4 --- /dev/null +++ b/tests/harness/corpus-costs/7437-duplex-from-signal.mjs.json @@ -0,0 +1 @@ +{ "plain": 2383, "san": 3146 } diff --git a/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json b/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json new file mode 100644 index 000000000..13b6f4240 --- /dev/null +++ b/tests/harness/corpus-costs/7438-duplex-from-signal.ts.json @@ -0,0 +1 @@ +{ "plain": 1777, "san": 2722 } diff --git a/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json b/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json new file mode 100644 index 000000000..a93aa2e95 --- /dev/null +++ b/tests/harness/corpus-costs/7439-readable-read-string-chunks.ts.json @@ -0,0 +1 @@ +{ "plain": 2578, "san": 3403 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index e74d9f60a..c26ef20c9 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 31, marker: "scr_runtime_abi_v31" }, + runtime_abi: { version: 32, marker: "scr_runtime_abi_v32" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/tests/harness/stream-entry-kinds.test.ts b/tests/harness/stream-entry-kinds.test.ts index ec89c5914..b0aa52294 100644 --- a/tests/harness/stream-entry-kinds.test.ts +++ b/tests/harness/stream-entry-kinds.test.ts @@ -1,14 +1,9 @@ /* Stream chunk kinds Node cannot oracle — scriptc-only. * - * A typed read() answers `Buffer | null`. When the read itself makes the - * stream hold strings (the first string a lazy Readable.from pull over a - * dynamic array produces, or setEncoding() inside _read), Node returns - * the string, which the typed result cannot hold, so the read throws the - * encoding fence instead (caught here, so the program exits normally). - * These pins keep that refusal exact and, under - * SCRIPTC_SAN=1 (ASan and the RC audit, like every harness lane), prove - * no string reaches Buffer code. The Node-observable cases are corpus - * programs (stream-dynamic-entry-kinds.*). + * Under SCRIPTC_SAN=1 (ASan and the RC audit, like every harness lane) + * these pins prove no string reaches Buffer code. The Node-observable + * cases are corpus programs (stream-dynamic-entry-kinds.*, and + * 7439-readable-read-string-chunks for read() of strings). */ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -73,71 +68,7 @@ function stripAsanFiberWarning(s: string): string { ); } -const READ_FENCE_MESSAGE = - "read() on a stream with an encoding set is not supported yet (consume 'data' events, which deliver strings)"; - describe(`typed stream reads of string chunks (scriptc-only${sanitize ? ", sanitized" : ""})`, () => { - test("a typed read() whose lazy pull yields the first string throws the encoding fence", async () => { - const r = await compileAndRun( - "typed-read-json-string", - `import { Readable } from "node:stream"; -const r = Readable.from(JSON.parse('["j1", "j2"]')); -try { - const chunk = r.read(); - console.log("hex", chunk === null ? "null" : chunk.toString("hex")); -} catch (err) { - console.log("caught", (err as Error).message); -} -`, - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - - test("a typed read() whose _read calls setEncoding() throws the encoding fence", async () => { - const r = await compileAndRun( - "typed-read-encoding-in-read", - `import { Readable } from "node:stream"; -const r = new Readable({ - read() { - this.setEncoding("utf8"); - this.push(Buffer.from("ab")); - this.push(null); - }, -}); -try { - const chunk = r.read(); - console.log("hex", chunk === null ? "null" : chunk.toString("hex")); -} catch (err) { - console.log("caught", (err as Error).message); -} -`, - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - - test("the JavaScript lane's read() on a JSON source throws the same fence", async () => { - const r = await compileAndRun( - "js-read-json-string", - `'use strict'; -const { Readable } = require('node:stream'); -const r = Readable.from(JSON.parse('["j1"]')); -r.once('readable', () => { - try { - const chunk = r.read(); - console.log('got', chunk === null ? 'null' : chunk.length); - } catch (err) { - console.log('caught', err.message); - } -}); -`, - "cjs", - ); - expect(r.exitCode).toBe(0); - expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); - }); - test("a dynamic source that mixes Buffers and strings yields each as it is, as Node does", async () => { const r = await compileAndRun( "mixed-dynamic-source", From 089ec6739b2d995e7b2d4de810a90db084f545f3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 16:00:41 -0700 Subject: [PATCH 052/169] Compile Iterable values Iterable maps to a union of the sources that keep their own representation (T[], Set, Map for [K, V] entries, string) and a generator over T that holds every iterator. An array stays the same multi-pass object; a generator stays single-pass. for-of gives the array arm its own array loop, so iterating an Iterable that holds an array runs at array speed. Spread, Array.from, the Set/Map constructors and Object.fromEntries consume it; optional Iterables narrow into it; generator functions may declare an Iterable return. Class iterables and typed arrays are refused at the conversion with a named diagnostic. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/limitations.mdx | 2 + .../language-features/iterable-values.mdx | 2 + .../src/frontend/lowering/class-iterables.ts | 8 +- .../lowering/containers/collection-methods.ts | 14 +- .../lowering/expressions/identifiers.ts | 11 + .../lowering/expressions/narrowing.ts | 17 + .../src/frontend/lowering/function-returns.ts | 4 + .../src/frontend/lowering/iterable-values.ts | 329 ++++++++++++++++++ .../src/frontend/lowering/lower-exprs.ts | 1 + .../src/frontend/lowering/lower-stmts.ts | 5 + .../compiler/src/frontend/lowering/lowerer.ts | 6 + packages/compiler/src/frontend/type-mapper.ts | 53 ++- packages/compiler/src/ir/ir.test.ts | 24 ++ packages/compiler/src/ir/ir.ts | 26 ++ packages/compiler/src/ir/validate.ts | 3 +- .../tests/corpus/7380-iterable-values.ts.json | 6 + .../7381-iterable-forof-semantics.ts.json | 6 + .../7382-iterable-generic-repository.ts.json | 6 + .../diagnostics/iterable-sources.ts.json | 6 + tests/corpus/7380-iterable-values.ts | 122 +++++++ tests/corpus/7381-iterable-forof-semantics.ts | 122 +++++++ .../7382-iterable-generic-repository.ts | 57 +++ tests/diagnostics/iterable-sources.ts | 17 + .../__snapshots__/iterable-sources.ts.txt | 13 + .../corpus-costs/7380-iterable-values.ts.json | 1 + .../7381-iterable-forof-semantics.ts.json | 1 + .../7382-iterable-generic-repository.ts.json | 1 + 27 files changed, 859 insertions(+), 4 deletions(-) create mode 100644 docs/content/limitations/language-features/iterable-values.mdx create mode 100644 packages/compiler/src/frontend/lowering/iterable-values.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json create mode 100644 tests/corpus/7380-iterable-values.ts create mode 100644 tests/corpus/7381-iterable-forof-semantics.ts create mode 100644 tests/corpus/7382-iterable-generic-repository.ts create mode 100644 tests/diagnostics/iterable-sources.ts create mode 100644 tests/harness/__snapshots__/iterable-sources.ts.txt create mode 100644 tests/harness/corpus-costs/7380-iterable-values.ts.json create mode 100644 tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json create mode 100644 tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index e132f27da..a86ed217e 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -24,6 +24,8 @@ title: "Limitations" ../limitations/language-features/async-iteration.mdx +../limitations/language-features/iterable-values.mdx + ../limitations/language-features/generics.mdx ../limitations/language-features/function-values.mdx diff --git a/docs/content/limitations/language-features/iterable-values.mdx b/docs/content/limitations/language-features/iterable-values.mdx new file mode 100644 index 000000000..2feedc02f --- /dev/null +++ b/docs/content/limitations/language-features/iterable-values.mdx @@ -0,0 +1,2 @@ +- **Iterable values:** A value typed `Iterable` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable`), a generator, or a Map, Set or array iterator. + Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable` and are reported at the conversion. diff --git a/packages/compiler/src/frontend/lowering/class-iterables.ts b/packages/compiler/src/frontend/lowering/class-iterables.ts index 8c58fc0bd..0f4d6c254 100644 --- a/packages/compiler/src/frontend/lowering/class-iterables.ts +++ b/packages/compiler/src/frontend/lowering/class-iterables.ts @@ -1,13 +1,18 @@ import type { IrExpr, IrType } from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall } from "./lower-classes.js"; +import { iterableGenerator, iterableUnionOf } from "./iterable-values.js"; /** The generator a class instance's `[Symbol.iterator]` returns, when that - * method is a generator method taking no arguments: `obj[Symbol.iterator]()`, + * method is a generator method taking no arguments (or the iterator an + * Iterable value opens over its current arm): `obj[Symbol.iterator]()`, * called once, as for-of, spread and Array.from call it. Null for other * values, and for classes whose iterator the program can replace at run * time (an instance property, a prototype write). */ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null { + // An Iterable value opens an iterator over its current arm. + const iterable = iterableUnionOf(lowerer, value.type); + if (iterable) return iterableGenerator(lowerer, value, iterable); if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null; const className = value.type.className; const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret; @@ -17,6 +22,7 @@ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr /** Whether values of `type` iterate through a generator method (the check * classGeneratorIterable makes, from the type alone). */ export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean { + if (iterableUnionOf(lowerer, type)) return true; if (type.kind !== "object") return false; const info = lowerer.classes.get(type.className); if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false; diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index d3169ec5c..e043daf68 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -22,6 +22,7 @@ import { isPrimitiveCollectionKey, isUnitType, typeEquals, + UNDEFINED_T, } from "../../../ir/ir.js"; import { COLLECTION_ITERATOR_METHODS, @@ -44,7 +45,7 @@ import { strCharsCall } from "./array-construction.js"; import { arrayElementRead } from "./array-iteration.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { classGeneratorIterable } from "../class-iterables.js"; -import { generatorDrain } from "../iterator-adapters.js"; +import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js"; /** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to * mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop @@ -1345,6 +1346,17 @@ export function mapFromSeedValue( const drained = generatorDrain(lowerer, generator, entry, loc); return drained ? mapFromSeedValue(lowerer, drained, mapT) : null; } + // A Set of [key, value] pairs walks its values like a generator of them. + if (seed.type.kind === "set" && seed.type.elem.kind === "record") { + const walk = lowerCollectionIteratorValue( + lowerer, + seed, + "values", + { kind: "generator", yieldT: seed.type.elem, retT: VOID, nextT: UNDEFINED_T }, + loc, + ); + return walk ? mapFromSeedValue(lowerer, walk, mapT) : null; + } if (seed.type.kind === "map") { if (!typeEquals(seed.type, mapT)) return null; return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 1f74cea42..13c8886f5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -41,6 +41,7 @@ import { runtimeOptionalUseOf, } from "../lower-exprs.js"; import { runtimeOptionalReceiverRead } from "./narrowing.js"; +import { iterableUnionOf } from "../iterable-values.js"; import { builtinGlobalValueAt } from "./property-access.js"; import { varRef } from "../../../ir/build.js"; import { @@ -321,6 +322,16 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // value arm before routing the member/element operation so dynamic // keys and builtin record walkers do not see a union receiver. const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr)); + // An optional Iterable proven present re-tags into the Iterable. + if ( + g.type.kind === "union" && + narrowed?.kind === "union" && + narrowed.unionId !== g.type.unionId && + iterableUnionOf(lowerer, narrowed) + ) { + const helper = lowerer.narrowedRetagHelper(expr, g.type.unionId, narrowed.unionId, loc); + if (helper) return { kind: "call", callee: helper, args: [ref], type: narrowed, loc }; + } const arithmeticUnion = g.type.kind === "union" && lowerer.unions.get(g.type.unionId)?.arms.length === 2 && diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 786f3a337..acac99610 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -46,6 +46,7 @@ import { } from "../lower-exprs.js"; import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js"; import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js"; +import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js"; /** `c ? a : b`. An explicit destination constructs each branch at that * layout before joining it. Without one, retain the expression's inferred @@ -692,6 +693,18 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc); if (hosted) return hosted; } + // An optional Iterable narrowed to the iterable union (`xs !== undefined`): + // re-tag into it, so for-of, spread and the other consumers see the + // Iterable itself. + if ( + narrowed?.kind === "union" && + narrowed.unionId !== expr.type.unionId && + iterableUnionOf(lowerer, narrowed) + ) { + const helper = lowerer.narrowedRetagHelper(node, expr.type.unionId, narrowed.unionId, expr.loc); + if (helper) + return { kind: "call", callee: helper, args: [expr], type: narrowed, loc: expr.loc }; + } // A checker type narrowed to a UNIT arm (the `=== undefined` branch) // also stays union-typed: a unit arm has no payload to extract, and // nothing useful reads such a value anyway. (Standalone undefined maps @@ -1524,6 +1537,10 @@ export function lowerAsExpression( // reach it (assertion-identity.ts). if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) return inner; + // `value as Iterable`: the value's own iterator, stepped lazily with + // each element checked against T (the generator arm's adapter). + const iterable = iterableSlotValue(lowerer, inner, target); + if (iterable) return iterable; if ( target.kind === "void" || !canDynCheckTo( diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 9500d6d2d..5ede3efe3 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -1,3 +1,4 @@ +import { iterableGeneratorArm } from "./iterable-values.js"; import { mapAsyncGeneratorAnnotation } from "../type-mapper.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; @@ -147,6 +148,9 @@ export function declaredReturnType( // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; let mappedReturn = lowerer.mapTypeOf(retTsType); + // A generator declared to return Iterable returns a generator over T. + if (mappedReturn && decl.asteriskToken) + mappedReturn = iterableGeneratorArm(lowerer, mappedReturn); if ( mappedReturn === null && decl.asteriskToken && diff --git a/packages/compiler/src/frontend/lowering/iterable-values.ts b/packages/compiler/src/frontend/lowering/iterable-values.ts new file mode 100644 index 000000000..cd5ef53c6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/iterable-values.ts @@ -0,0 +1,329 @@ +/* `Iterable` values. The type maps to the iterable union + * (type-mapper.ts iterableUnionType): an array, Set, Map or string keeps + * its own arm, so the value stays the same multi-pass object (identity, + * inspection and typeof are the source's own), and every iterator — a + * generator, an array, Map or Set iterator, a checked-dynamic iterator — + * enters the generator arm, single-pass as in JavaScript. + * + * Consumers open an iterator over the current arm the way for-of's + * GetIterator does: an array, Set, Map or string arm starts a fresh live + * walk (the sources' own iterator helpers), the generator arm is itself. + * for-of gives the array arm its own array loop, so iterating an Iterable + * that holds an array runs at array-loop speed. */ +import * as ts from "../ts7/adapter.js"; +import { locOf } from "../program.js"; +import { varRef, numLit } from "../../ir/build.js"; +import { + BOOL, + F64, + iterableArmsElem, + typeEquals, + type IrExpr, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { typeKey } from "../type-mapper.js"; +import { keyedHelperName } from "./stable-names.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { noLoweringDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { generatorLoopBinding, lowerForOfGenerator, type GenType } from "./lower-generators.js"; +import { + iteratorSlotAdapter, + lowerArrayIteratorValue, + lowerCollectionIteratorValue, + lowerStringIteratorValue, +} from "./iterator-adapters.js"; +import { arrayValueRead, arrayValueType } from "./array-values.js"; + +/** An iterable union and the tags of its arms. */ +export interface IterableUnion { + unionId: string; + type: IrType & { kind: "union" }; + elem: IrType; + arms: readonly IrType[]; + /** The generator arm: a sync generator over `elem`. */ + gen: GenType; +} + +/** The iterable union `t` names, or null. */ +export function iterableUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { + if (t.kind !== "union") return null; + const arms = lowerer.unions.get(t.unionId)?.arms; + if (!arms) return null; + const elem = iterableArmsElem(arms); + // An optional Iterable (`Iterable | undefined`) is consumed only once + // narrowed to the iterable union itself. + if (elem === null || arms.some((arm) => arm.kind === "undefinedT" || arm.kind === "nullT")) + return null; + const gen = arms.find((arm) => arm.kind === "generator") as GenType; + return { unionId: t.unionId, type: t, elem, arms, gen }; +} + +/** The generator arm of an iterable union (what a generator function + * declared to return Iterable returns), or `t` itself. */ +export function iterableGeneratorArm(lowerer: Lowerer, t: IrType): IrType { + return iterableUnionOf(lowerer, t)?.gen ?? t; +} + +/** A value flowing into an iterable-typed slot: a source arm wraps as + * itself; a generator over other channel types or a checked-dynamic + * iterator (a Map or Set iterator) adapts into the generator arm. Null + * when neither applies, leaving the ordinary conversion fences to refuse + * other sources (class iterables, typed arrays, other element types). */ +export function iterableSlotValue(lowerer: Lowerer, expr: IrExpr, expected: IrType): IrExpr | null { + const info = iterableUnionOf(lowerer, expected); + if (info === null || expr.type.kind === "union") return null; + const wrap = (value: IrExpr): IrExpr => ({ + kind: "unionWrap", + unionId: info.unionId, + tag: lowerer.armTag(info.unionId, value.type), + value, + type: info.type, + loc: expr.loc, + }); + if (lowerer.armTag(info.unionId, expr.type) >= 0) return wrap(expr); + if (expr.type.kind === "generator" || expr.type.kind === "dyn") { + const adapted = iteratorSlotAdapter(lowerer, expr, info.gen); + if (adapted && typeEquals(adapted.type, info.gen)) return wrap(adapted); + } + // Class instances (their own [Symbol.iterator]) and typed arrays have no + // arm: refuse them here, by name. + if (expr.type.kind === "object" || expr.type.kind === "bytes") { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + expr.loc, + `an Iterable<${lowerer.fmt(info.elem)}> holding a '${lowerer.fmt(expr.type)}' (an Iterable value holds an array, Set, Map, string, or iterator)`, + ), + ); + throw new PoisonError(); + } + return null; +} + +/** GetIterator on an iterable union value: an interned helper answering a + * generator over the current arm — a fresh live walk of an array, Set, Map + * or string, or the generator arm itself. */ +export function iterableGenerator(lowerer: Lowerer, value: IrExpr, info: IterableUnion): IrExpr { + const loc = value.loc; + const key = `iterableOf:${typeKey(info.type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const u = varRef("u.0", info.type, loc); + const body: IrStmt[] = []; + let genTag = -1; + info.arms.forEach((arm, tag) => { + if (arm.kind === "generator") { + genTag = tag; + return; + } + const narrowed: IrExpr = { + kind: "unionNarrow", + unionId: info.unionId, + tag, + value: u, + type: arm, + loc, + }; + const walk = + arm.kind === "array" + ? lowerArrayIteratorValue(lowerer, narrowed, "values", info.gen, loc) + : arm.kind === "set" + ? lowerCollectionIteratorValue(lowerer, narrowed, "values", info.gen, loc) + : arm.kind === "map" + ? lowerCollectionIteratorValue(lowerer, narrowed, "entries", info.gen, loc) + : arm.kind === "string" + ? lowerStringIteratorValue(lowerer, narrowed, info.gen, loc) + : null; + if (walk === null || !typeEquals(walk.type, info.gen)) { + lowerer.pushDiag( + noLoweringDiag( + `iterating an Iterable<${lowerer.fmt(info.elem)}> that holds a ${arm.kind}`, + loc, + ), + ); + throw new PoisonError(); + } + body.push({ + kind: "if", + cond: { + kind: "unionIsTag", + unionId: info.unionId, + tag, + negated: false, + value: u, + type: BOOL, + loc, + }, + then: [{ kind: "return", value: walk, loc }], + else_: null, + loc, + }); + }); + body.push({ + kind: "return", + value: { + kind: "unionNarrow", + unionId: info.unionId, + tag: genTag, + value: u, + type: info.gen, + loc, + }, + loc, + }); + name = keyedHelperName(lowerer, "%iterable.of", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "u.0", name: "u", type: info.type }], + returnType: info.gen, + locals: [{ id: "u.0", name: "u", type: info.type, mutable: false }], + body, + loc, + }); + } + return { kind: "call", callee: name, args: [value], type: info.gen, loc }; +} + +/** Whether a loop body declares a class (a class has one lowering per + * declaration, so its body cannot lower twice). */ +function declaresClass(body: ts.Statement): boolean { + let found = false; + ts.walkPreorder(body, (node) => { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** `for (x of iterable)` over an iterable union: + * + * { const %src = ; + * if (%src is the array arm) { + * const %arr = ; + * for (let %cur = 0; %cur < %arr.length; %cur++) { const x = %arr[%cur]; } + * } else { + * for (const x of GetIterator(%src)) // the generator for-of + * } + * } + * + * The array arm walks exactly like an array for-of (live length, the + * backend's dense-array loop forms), so an Iterable holding an array + * iterates at array speed; every other arm drives its iterator with the + * generator for-of's IteratorClose rules. The body lowers once for each + * loop; a body that declares a class lowers only once, in the generator + * for-of over every arm. */ +export function lowerForOfIterable( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + iterable: IrExpr, + info: IterableUnion, + labels?: string[], +): IrStmt { + const loc = locOf(stmt); + const generatorLoop = (source: IrExpr): IrStmt => + lowerForOfGenerator( + lowerer, + stmt, + iterableGenerator(lowerer, source, info) as IrExpr & { type: GenType }, + labels, + ); + if (stmt.awaitModifier || declaresClass(stmt.statement)) return generatorLoop(iterable); + const arrayTag = info.arms.findIndex((arm) => arm.kind === "array"); + const arrT = info.arms[arrayTag]!; + lowerer.scopes.push(new Map()); + try { + const src = lowerer.declareHiddenLocal("%iterableSource", info.type); + const srcRef = (): IrExpr => varRef(src.id, info.type, loc); + const arr = lowerer.declareHiddenLocal("%arrayIterator", arrT); + const cursor = lowerer.declareHiddenLocal("%arrayCursor", F64); + cursor.mutable = true; + const arrRef = varRef(arr.id, arrT, loc); + const curRef = varRef(cursor.id, F64, loc); + // The element: a hole reads undefined, which an undefined-armed T + // holds; for other element types the read traps, as typed reads do. + const element: IrExpr = typeEquals(arrayValueType(lowerer, info.elem), info.elem) + ? arrayValueRead(lowerer, arrRef, curRef, info.elem, loc) + : { kind: "arrayGet", arr: arrRef, index: curRef, type: info.elem, loc }; + const arrayLoop = ((): IrStmt => { + lowerer.scopes.push(new Map()); + try { + const binding = generatorLoopBinding(lowerer, stmt, element, info.elem); + const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + return { + kind: "for", + init: { kind: "varDecl", localId: cursor.id, init: numLit(0, loc), loc }, + cond: { + kind: "bin", + op: "<", + left: curRef, + right: { + kind: "arrIntrinsic", + method: "length", + receiver: arrRef, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + update: { + kind: "assign", + localId: cursor.id, + value: { kind: "bin", op: "+", left: curRef, right: numLit(1, loc), type: F64, loc }, + loc, + }, + body: [...binding, ...body], + ...(labels && { labels }), + loc, + }; + } finally { + lowerer.scopes.pop(); + } + })(); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: src.id, init: iterable, loc }, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: info.unionId, + tag: arrayTag, + negated: false, + value: srcRef(), + type: BOOL, + loc, + }, + then: [ + { + kind: "varDecl", + localId: arr.id, + init: { + kind: "unionNarrow", + unionId: info.unionId, + tag: arrayTag, + value: srcRef(), + type: arrT, + loc, + }, + loc, + }, + arrayLoop, + ], + else_: [generatorLoop(srcRef())], + loc, + }, + ], + loc, + }; + } finally { + lowerer.scopes.pop(); + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b5acaf9e6..a6adf736c 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -2092,6 +2092,7 @@ export function typeofAnswer(arm: IrType): string | null { case "array": case "map": case "set": + case "generator": case "regex": case "bytes": case "url": diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index d49c7643e..be287c1a7 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -239,6 +239,7 @@ import { npmStaticPackageOfPath } from "../npm-static.js"; import { fenceNodeModuleMutation } from "./lower-node-module.js"; import { fencePerfEntryKeyWalk } from "./builtins/performance.js"; import { classGeneratorIterable } from "./class-iterables.js"; +import { iterableUnionOf, lowerForOfIterable } from "./iterable-values.js"; interface UsingRegistration { acquire: IrStmt[]; @@ -9637,6 +9638,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } iterSrc ??= stmt.expression; let iterable = lowerer.lowerExpr(iterSrc); + { + const iterableUnion = iterableUnionOf(lowerer, iterable.type); + if (iterableUnion) return lowerForOfIterable(lowerer, stmt, iterable, iterableUnion, labels); + } let tupleSource: { shapeId: string; fields: { name: string; type: IrType }[]; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 34b345c94..b0e6c4870 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -67,6 +67,7 @@ import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; +import { iterableSlotValue } from "./iterable-values.js"; import type { FieldLift } from "./coercions/structural-plans.js"; import { lowerBuiltinCall } from "./builtin-calls.js"; import { CoercionState } from "./coercions/state.js"; @@ -9450,6 +9451,11 @@ export class Lowerer { const adapted = iteratorSlotAdapter(this, expr, expected); if (adapted) return adapted; } + // Iterable-typed slots: other iterators enter the generator arm. + if (expected.kind === "union" && !typeEquals(expr.type, expected)) { + const iterable = iterableSlotValue(this, expr, expected); + if (iterable) return iterable; + } // an any-typed slot marshals implicitly (tsc allows the assignment; // the marshal is where its semantics live). OUT: an 'any' value diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 14379f30f..017ef98ac 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -24,6 +24,7 @@ import { isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, + iterableArmsElem, isUnitType, mapOf, NULL_T, @@ -3935,6 +3936,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ...channels, }; } + // Iterable: the iterable union (iterableUnionType) — the source value + // itself in its own arm, so an array stays the same multi-pass object and + // a generator stays single-pass. + if (isStdlibInterface("Iterable")) { + const arg = checker.getTypeArguments(widened as ts.TypeReference)[0]; + const elem = arg ? mapType(arg, ctx) : null; + return elem ? iterableUnionType(elem, ctx.shapes, unions) : null; + } // IteratorResult — the checker's type of `g.next()` (an // alias for IteratorYieldResult | IteratorReturnResult): // maps to the SAME interned record genResume answers, so `const r = @@ -4989,7 +4998,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { a.kind === "union" || a.kind === "date" || a.kind === "dyn" || - a.kind === "generator", + // A generator arm only comes from an Iterable (an optional + // one keeps its unit arms beside the iterable union's). + (a.kind === "generator" && iterableArmsElem(arms) === null), ) || !maybePromiseUnionOk(parts, arms, ctx) ) { @@ -6432,6 +6443,46 @@ export function unitOnlyUnion(unions: UnionRegistry): IrType { return { kind: "union", unionId: unions.intern(arms) }; } +/** `Iterable`: a union whose arms are the sources that keep their own + * representation — `T[]`, a `Set`, a `Map` (T the `[K, V]` + * tuple), a string (T string) — and a generator over T, which also holds + * every other iterator (Map/Set and array iterators adapt into one). The + * generator arm marks the union as iterable (iterableArmsElem): no other + * union has one. Null when T has no array or generator form. */ +export function iterableUnionType( + elem: IrType, + shapes: ShapeRegistry, + unions: UnionRegistry, +): IrType | null { + if (elem.kind === "void" || elem.kind === "generator" || !isSupportedArrayElem(elem)) return null; + if (!genResultRecord(elem, VOID, shapes, unions)) return null; + const arms: IrType[] = [ + { kind: "array", elem }, + { kind: "generator", yieldT: elem, retT: VOID, nextT: UNDEFINED_T }, + ]; + const elemArms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined; + if (isSupportedSetElem(elem, elemArms)) arms.push({ kind: "set", elem }); + if (elem.kind === "string") arms.push(STRING); + if (elem.kind === "record") { + const shape = shapes.get(elem.shapeId); + const [k, v] = shape?.fields ?? []; + if ( + shape?.tuple && + shape.fields.length === 2 && + k?.name === "0" && + v?.name === "1" && + isSupportedMapKey( + k.type, + k.type.kind === "union" ? unions.get(k.type.unionId)?.arms : undefined, + ) && + isSupportedMapValue(v.type) + ) + arms.push({ kind: "map", key: k.type, value: v.type }); + } + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: unions.intern(arms) }; +} + /** The IteratorResult record of a generator's value channels: `{ done: * boolean, value: V }` where V is dyn (an any/unknown channel) or the * canonical union of the yield arms, the return arms (when the return diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 52ef9b9c1..d62c9c83c 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -10,6 +10,7 @@ import { canDynCheckTo, type IrBytesElem, isJsonSafeType, + iterableArmsElem, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, @@ -22,6 +23,29 @@ import { typeKey, } from "./ir.js"; +describe("iterable unions", () => { + const gen = (yieldT: IrType): IrType => ({ + kind: "generator", + yieldT, + retT: { kind: "void" }, + nextT: { kind: "undefinedT" }, + }); + + test("names the element type of an Iterable union", () => { + expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "set", elem: F64 }])).toEqual(F64); + expect(iterableArmsElem([arrayOf(STRING), gen(STRING), STRING])).toEqual(STRING); + expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "undefinedT" }])).toEqual(F64); + }); + + test("rejects unions without exactly one plain generator arm over the same element", () => { + expect(iterableArmsElem([arrayOf(F64), STRING])).toBeNull(); + expect(iterableArmsElem([arrayOf(STRING), gen(F64)])).toBeNull(); + expect(iterableArmsElem([arrayOf(F64), gen(F64), gen(STRING)])).toBeNull(); + expect(iterableArmsElem([gen(F64), STRING])).toBeNull(); + expect(iterableArmsElem([arrayOf(F64), { ...gen(F64), nextT: F64 } as IrType])).toBeNull(); + }); +}); + describe("IR kind sets", () => { test("keeps procStream as the scalar handle exception", () => { expect(HANDLE_KINDS.has("procStream")).toBe(true); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4a395feb2..27f65e5b4 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -730,6 +730,32 @@ function isCollectionValueKey(t: IrType): boolean { ); } +/** The element type of an ITERABLE union (`Iterable`, + * iterableUnionType): its one generator arm — a synchronous generator over + * T that returns nothing and takes no `.next()` values — beside source arms + * (T[], Set, a Map of [K, V] entries, a string), and the undefined and + * null arms of an optional Iterable. A generator arm is legal only in such + * a union. Null for every other union. */ +export function iterableArmsElem(arms: readonly IrType[]): IrType | null { + let elem: IrType | null = null; + for (const arm of arms) { + if (arm.kind !== "generator") continue; + if (elem !== null || arm.async || arm.retT.kind !== "void" || arm.nextT.kind !== "undefinedT") + return null; + elem = arm.yieldT; + } + if (elem === null) return null; + for (const arm of arms) { + if (arm.kind === "generator" || arm.kind === "undefinedT" || arm.kind === "nullT") continue; + if (arm.kind === "array" || arm.kind === "set") { + if (!typeEquals(arm.elem, elem)) return null; + } else if (arm.kind === "string") { + if (elem.kind !== "string") return null; + } else if (arm.kind !== "map") return null; + } + return elem; +} + /** Set elements and Map keys share storage, equality and ownership rules. */ export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean { return isSupportedMapKey(t, unionArms); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 163c2a41c..217948641 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -18,6 +18,7 @@ import type { } from "./ir.js"; import { arrayOf, + iterableArmsElem, BYTES_ELEMENT_NAME, BOOL, BYTES_U8, @@ -801,7 +802,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { arm.kind === "union" || arm.kind === "dyn" || arm.kind === "date" || - arm.kind === "generator" + (arm.kind === "generator" && iterableArmsElem(u.arms) === null) ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json new file mode 100644 index 000000000..cf42c1888 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-iterable-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..824dfe11c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-iterable-forof-semantics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..7ac2b807f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-iterable-generic-repository.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json new file mode 100644 index 000000000..1b47e8dd2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/iterable-sources.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7380-iterable-values.ts b/tests/corpus/7380-iterable-values.ts new file mode 100644 index 000000000..0c4284285 --- /dev/null +++ b/tests/corpus/7380-iterable-values.ts @@ -0,0 +1,122 @@ +// Iterable values: an array, Set, Map or string stays the same object +// (multi-pass, same identity, same inspection), and a generator or a +// Map/Set/array iterator stays single-pass. for-of, spread, Array.from and +// the Set/Map constructors consume every source. + +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} + +function* nums(): Generator { + yield 10; + yield 20; +} + +const arr = [1, 2, 3]; +const scores = new Map([ + ["a", 4], + ["b", 5], +]); +console.log(sum(arr), sum(new Set([1, 1, 2])), sum(scores.values()), sum(nums()), sum(arr.values())); + +// An array stays the same multi-pass object. +const it: Iterable = arr; +console.log(sum(it), sum(it), it === arr, typeof it, it); + +// A generator stays single-pass. +const once: Iterable = nums(); +console.log(sum(once), sum(once), typeof once); + +console.log([...it], Array.from(nums() as Iterable), new Set(it).size); + +// A function returning either a collection iterator or a fresh array. +function pick(rows: Map, ids: string[] | null): Iterable { + if (ids === null) return rows.values(); + const out: number[] = []; + for (const id of ids) out.push(rows.get(id) ?? 0); + return out; +} +let total = 0; +for (const v of pick(scores, null)) total += v; +for (const v of pick(scores, ["b", "b", "z"])) total += v; +console.log(total); + +// Strings iterate by code point; Maps iterate [key, value] entries. +function upper(s: Iterable): string[] { + const out: string[] = []; + for (const w of s) out.push(w.toUpperCase()); + return out; +} +console.log(upper("héllo😀"), upper(["a", "b"]), upper(new Set(["x"]))); +function pairs(p: Iterable<[string, number]>): string { + let out = ""; + for (const [k, v] of p) out += `${k}=${v};`; + return out; +} +console.log(pairs(scores), pairs([["x", 1]]), pairs(scores.entries()), pairs(new Set<[string, number]>([["s", 9]]))); + +// Constructors and Object.fromEntries take any Iterable. +function toMap(p: Iterable<[string, number]>): Map { + return new Map(p); +} +function toSet(xs: Iterable): Set { + return new Set(xs); +} +console.log(toMap([["a", 1], ["b", 2]]), toMap(scores), Object.fromEntries(toMap(new Set<[string, number]>([["c", 3]])))); +console.log(toSet([3, 3, 4]), toSet(new Set([9])), toSet(nums())); + +// Fields, generator functions declared to return Iterable, nested Iterables. +class Holder { + source: Iterable; + constructor(source: Iterable) { + this.source = source; + } + joined(): string { + let out = ""; + for (const s of this.source) out += s; + return out; + } +} +const holder = new Holder(["a", "b"]); +console.log(holder.joined(), holder.joined()); +holder.source = new Set(["x", "y"]); +console.log(holder.joined(), holder); + +function* range(n: number): Iterable { + for (let i = 0; i < n; i++) yield i; +} +console.log([...range(4)], sum(range(5))); + +const grid: Iterable[] = [[1, 2], new Set([3]), range(3)]; +let cells = 0; +for (const row of grid) for (const c of row) cells += c; +console.log(cells, grid.length); + +// A checked cast of an iterator value. +const entries = new Map([["k", 1]]).entries() as Iterable<[string, number]>; +console.log(toMap(entries)); + +// Optional Iterables narrow to the Iterable once proven present. +function totalOf(xs?: Iterable): number { + if (xs === undefined) return -1; + let s = 0; + for (const x of xs) s += x; + return s; +} +function orEmpty(xs: Iterable | null): number[] { + return xs === null ? [] : [...xs]; +} +console.log(totalOf(), totalOf([1, 2]), totalOf(new Set([5])), orEmpty(null), orEmpty(range(2))); +const byName = new Map>([ + ["a", [1, 2]], + ["b", new Set([3])], +]); +for (const k of ["a", "b", "c"]) { + const got = byName.get(k); + if (got !== undefined) console.log(k, [...got]); + else console.log(k, "none"); +} +const top = byName.get("a"); +console.log(top !== undefined ? [...top] : "none", byName); diff --git a/tests/corpus/7381-iterable-forof-semantics.ts b/tests/corpus/7381-iterable-forof-semantics.ts new file mode 100644 index 000000000..10f1d1d2f --- /dev/null +++ b/tests/corpus/7381-iterable-forof-semantics.ts @@ -0,0 +1,122 @@ +// for-of over Iterable keeps the semantics of the held source: an array +// is walked live, a generator is closed on break, return and throw (its +// finally blocks run) but not when it is exhausted or throws itself, and +// labels, closures, await and yield work in the loop body. + +function* noisy(tag: string): Generator { + try { + yield 1; + yield 2; + yield 3; + } finally { + console.log("closed", tag); + } +} + +const early: Iterable = noisy("break"); +for (const v of early) { + if (v === 2) break; + console.log("v", v); +} + +function firstOver(src: Iterable, n: number): number | undefined { + for (const x of src) if (x > n) return x; + return undefined; +} +console.log(firstOver([1, 5, 9], 4), firstOver(noisy("return"), 1), firstOver(noisy("exhausted"), 9)); + +try { + for (const v of noisy("throw") as Iterable) { + if (v === 1) throw new Error("boom"); + } +} catch (e) { + console.log("caught", (e as Error).message); +} + +function* failing(): Generator { + try { + yield 1; + throw new RangeError("generator failed"); + } finally { + console.log("failing finally"); + } +} +try { + for (const v of failing() as Iterable) console.log("got", v); +} catch (e) { + console.log(String(e)); +} + +// The array is read live: elements pushed during the walk are visited. +const grow: number[] = [1, 2]; +let visits = 0; +for (const v of grow as Iterable) { + if (grow.length < 5) grow.push(v * 10); + visits++; +} +console.log(visits, grow); + +// Holes read undefined when T holds it. +function show(xs: Iterable): string { + const out: string[] = []; + for (const x of xs) out.push(String(x)); + return out.join(","); +} +console.log(show([1, , 3]), show([undefined, 2])); + +// Labels, closures over the per-iteration binding, try/finally, a class +// declared in the body, and an existing binding as the loop target. +function run(src: Iterable, tag: string): void { + const fns: (() => number)[] = []; + let acc = 0; + outer: for (const x of src) { + fns.push(() => x * 10); + try { + if (x === 2) continue outer; + class Local { + v = x; + twice(): number { + return this.v * 2; + } + } + acc += new Local().twice(); + const inner = (y: number): number => y + x; + acc += inner(1); + } finally { + acc += 100; + } + } + console.log(tag, acc, fns.map((f) => f()).join(",")); +} +function* three(): Generator { + yield 1; + yield 2; + yield 3; +} +run([1, 2, 3], "array"); +run(three(), "generator"); +run(new Set([1, 2, 3]), "set"); + +let product = 0; +outer: for (const a of [1, 2, 3] as Iterable) { + for (const b of new Set([10, 20]) as Iterable) { + if (b === 20) continue outer; + if (a === 3) break outer; + product += a * b; + } +} +let target = 0; +for (target of [7, 8] as Iterable) { +} +console.log(product, target); + +// await and yield inside the loop body. +async function asyncSum(src: Iterable): Promise { + let s = 0; + for (const x of src) s += await Promise.resolve(x); + return s; +} +function* doubled(src: Iterable): Generator { + for (const x of src) yield x * 2; +} +asyncSum([4, 5]).then((v) => console.log("async", v, [...doubled([1, 2])], [...doubled(new Set([7]))])); diff --git a/tests/corpus/7382-iterable-generic-repository.ts b/tests/corpus/7382-iterable-generic-repository.ts new file mode 100644 index 000000000..3e9e62742 --- /dev/null +++ b/tests/corpus/7382-iterable-generic-repository.ts @@ -0,0 +1,57 @@ +// A generic repository whose query candidates are an Iterable: the +// live row map's values() iterator when nothing is indexed, or an array of +// index hits. Methods returning Iterable keep the class compilable. + +interface Row { + readonly id: string; + readonly tag: string; + readonly n: number; +} + +class Repository { + private readonly rows = new Map(); + private readonly index = new Map(); + + private readonly key: (row: T) => string; + + constructor(key: (row: T) => string) { + this.key = key; + } + + insert(row: T): void { + this.rows.set(row.id, row); + const k = this.key(row); + const ids = this.index.get(k); + if (ids === undefined) this.index.set(k, [row.id]); + else ids.push(row.id); + } + + all(): Iterable { + return this.rows.values(); + } + + candidates(key: string | null): Iterable { + if (key === null) return this.rows.values(); + const out: T[] = []; + for (const id of this.index.get(key) ?? []) { + const row = this.rows.get(id); + if (row !== undefined) out.push(row); + } + return out; + } + + query(key: string | null, where: (row: T) => boolean): T[] { + const matches: T[] = []; + for (const row of this.candidates(key)) if (where(row)) matches.push(row); + return matches; + } +} + +const repo = new Repository((row) => row.tag); +for (let i = 0; i < 12; i++) repo.insert({ id: `r${i}`, tag: i % 3 === 0 ? "red" : "blue", n: i * i }); +console.log(repo.query(null, (r) => r.n > 50).map((r) => r.id).join(",")); +console.log(repo.query("red", (r) => r.n % 2 === 0).map((r) => r.id).join(",")); +console.log(repo.query("green", () => true).length, [...repo.all()].length); +let sum = 0; +for (const row of repo.candidates("blue")) sum += row.n; +console.log(sum, Array.from(repo.candidates(null)).slice(0, 2)); diff --git a/tests/diagnostics/iterable-sources.ts b/tests/diagnostics/iterable-sources.ts new file mode 100644 index 000000000..c93029cce --- /dev/null +++ b/tests/diagnostics/iterable-sources.ts @@ -0,0 +1,17 @@ +// An Iterable value holds an array, Set, Map, string or iterator. A +// class instance with its own [Symbol.iterator] and a typed array have no +// representation there, so the conversion site reports them by name. +class Countdown implements Iterable { + *[Symbol.iterator](): Iterator { + yield 2; + yield 1; + } +} +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} +console.log(sum(new Countdown())); +console.log(sum(new Uint8Array([1, 2]))); +export const marker: number = 1; diff --git a/tests/harness/__snapshots__/iterable-sources.ts.txt b/tests/harness/__snapshots__/iterable-sources.ts.txt new file mode 100644 index 000000000..cac17a49e --- /dev/null +++ b/tests/harness/__snapshots__/iterable-sources.ts.txt @@ -0,0 +1,13 @@ +iterable-sources.ts:15:17 - error SC1090: an Iterable holding a 'Countdown' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 14 | } + 15 | console.log(sum(new Countdown())); + | ^~~~~~~~~~~~~~~ + 16 | console.log(sum(new Uint8Array([1, 2]))); + +iterable-sources.ts:16:17 - error SC1090: an Iterable holding a 'Uint8Array' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 15 | console.log(sum(new Countdown())); + 16 | console.log(sum(new Uint8Array([1, 2]))); + | ^~~~~~~~~~~~~~~~~~~~~~ + 17 | export const marker: number = 1; \ No newline at end of file diff --git a/tests/harness/corpus-costs/7380-iterable-values.ts.json b/tests/harness/corpus-costs/7380-iterable-values.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7380-iterable-values.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } From dc2b238df7ed9337abce8751b157aca891427c8d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:02:51 -0500 Subject: [PATCH 053/169] Revert "Dispatch checked-dynamic method calls on method ids (#1851)" --- benchmarks/runtime/README.md | 1 - .../runtime/workloads/dynamic-methods.json | 6 - .../runtime/workloads/dynamic-methods.mjs | 36 --- packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/emit-module.ts | 2 - packages/compiler/src/backend/llvm/emitter.ts | 14 - .../compiler/src/backend/llvm/expr-dynamic.ts | 8 +- packages/compiler/src/backend/llvm/js-ic.ts | 2 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_bytes_dyn.c | 42 ++- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 22 +- packages/runtime/src/scr_dyn_invoke.c | 276 ++++++++---------- packages/runtime/src/scr_dyn_methods.h | 160 ---------- packages/runtime/src/scr_dyn_methods.test.ts | 10 - packages/runtime/src/scr_worker_events.c | 40 ++- scripts/gen-dyn-methods.mjs | 101 ------- scripts/zero-cost-compare.mjs | 4 +- .../self-hosting-native-toolchain.test.ts | 2 +- 25 files changed, 188 insertions(+), 562 deletions(-) delete mode 100644 benchmarks/runtime/workloads/dynamic-methods.json delete mode 100644 benchmarks/runtime/workloads/dynamic-methods.mjs delete mode 100644 packages/runtime/src/scr_dyn_methods.h delete mode 100644 packages/runtime/src/scr_dyn_methods.test.ts delete mode 100644 scripts/gen-dyn-methods.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 4f152e4bb..e177e36c0 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -12,7 +12,6 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `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 | | `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-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json deleted file mode 100644 index fa27f4ea8..000000000 --- a/benchmarks/runtime/workloads/dynamic-methods.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "entry": "workloads/dynamic-methods.mjs", - "args": [ - "300000" - ] -} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs deleted file mode 100644 index bbf217701..000000000 --- a/benchmarks/runtime/workloads/dynamic-methods.mjs +++ /dev/null @@ -1,36 +0,0 @@ -// Untyped JavaScript calling builtin methods on values it never declares -// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array -// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number -// toFixed, and calls of an object's own methods. Argument: the number of -// iterations. -const rounds = Number(process.argv[2] ?? "300000"); -const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]; -const counts = new Map(); -const seen = new Set(); -const stack = []; -const recent = []; -const tally = { - total: 0, - add(value) { - this.total += value; - return this.total; - }, -}; -let checksum = 0; -for (let i = 0; i < rounds; i++) { - const word = words[i % words.length]; - const head = word.slice(0, 3).toUpperCase(); - const key = head.padStart(4, "-"); - if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1; - counts.set(key, (counts.get(key) ?? 0) + 1); - seen.add(i % 97); - if (seen.has(i % 89)) checksum += 2; - stack.push(i); - if (stack.length > 32) checksum += stack.pop() % 3; - recent.push(word); - if (recent.length > 4) recent.shift(); - if (recent.includes("gamma")) checksum += recent.indexOf("gamma"); - checksum += tally.add(i % 5) % 11; - if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0; -} -console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total); diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index a1b673630..a7a5e718a 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v28"); + expect(error?.stderr).toContain("scr_runtime_abi_v27"); }); }); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index fa5df1f38..372a601ee 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -875,8 +875,6 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); } if (emitter.mapOpsConsts.size > 0) out.push(``); - for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`); - if (emitter.methodIdSlots.size > 0) out.push(``); for (const line of ffiCallbacks.globals) out.push(line); if (ffiCallbacks.globals.length > 0) out.push(``); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index f34345ed0..48e20fe5a 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -832,10 +832,6 @@ export class LlEmitter { readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ readonly mapOpsConsts = new Map(); - /** Method-id slots of checked-dynamic method calls - * (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the - * runtime fills with the name's id on the first call. */ - readonly methodIdSlots = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ @@ -2474,16 +2470,6 @@ export class LlEmitter { return `@${c.sym}`; } - /** The method-id slot for a constant method name (methodIdSlots). */ - methodIdSlot(name: string): string { - let sym = this.methodIdSlots.get(name); - if (!sym) { - sym = this.contentSymbol("sc_mid_", name); - this.methodIdSlots.set(name, sym); - } - return `@${sym}`; - } - needBadTag(): void { this.needsBadTag = true; } diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 434435eae..9af677586 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -250,10 +250,8 @@ export function emitDynamicExpr( } // Buffer.prototype method names take their scr_bytes_dyn.c entry, which // runs them on Buffer receivers and dispatches any other receiver. - // Each call passes its method name's id slot, so only the first call - // looks the name up. - const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id"; - host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); + const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -262,7 +260,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 7516673cd..45c18b803 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { /** dynInvoke's method read on an ordinary object through the site's * cache, else scr_dyn_prepare_method. Returns the owned method (or NULL, - * which scr_dyn_invoke_prepared_id answers with the full dispatch). */ + * which scr_dyn_invoke_prepared answers with the full dispatch). */ export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string { const B = host.B; const method = e.method; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index a616dbec2..3a41d9176 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 28 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const; +export const RUNTIME_ABI_VERSION = 27 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index c2536ede8..af61e5229 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 33b1ca38e..d09509911 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 71b9570b4..35a747d60 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v28"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v27"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 3d22b2360..631750cdf 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 909412bc7..0e81033ad 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index c492d43b1..280aa423c 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,9 +509,8 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -519,45 +518,40 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -590,5 +584,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); } diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 9bd5abf8c..a3ac43cc0 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v28(void) {} +void scr_runtime_abi_v27(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 35332d0ca..fea020d16 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v28(void); +void scr_runtime_abi_v27(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index e99763f0c..bc2786e06 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1284,19 +1284,15 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for a call site whose method name is a constant: - * *mid_slot, the site's -1-initialized slot, caches the name's method id - * after the first call, so later calls skip the name lookup. */ -ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared_id for Buffer.prototype method names - * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs - * the method, any other takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), + * one entry per method family: a Buffer receiver runs the method, any other + * takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index dda241bd6..e978ac7aa 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -8,7 +8,6 @@ * scr_json.c; this unit uses only its public surface plus ScrJsonBuf. */ #include "scr_runtime.h" -#include "scr_dyn_methods.h" #include #include @@ -550,11 +549,7 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return scr_dyn_retain(scr_dyn_undefined()); } -/* The dispatch below maps a call's method name to an id once - * (dyn_method_of, scr_dyn_methods.h) and its branches compare ids. Names - * compare by content, and any name the branches do not test is DYN_M_NONE. - * scripts/gen-dyn-methods.mjs writes the header from the DYN_M_ ids used - * here. */ +static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; } /* Handle receivers: hand the whole call to the tag's ops (installed by * the owning unit at main() — a missing install is an internal error the @@ -697,34 +692,29 @@ static bool dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) { /* Names each prototype declares BEYOND what's implemented here — these * throw the "not supported yet" Error instead of mis-answering "is not a * function". */ -static bool dyn_arr_proto_unimpl(int m) { - return m == DYN_M_toString || m == DYN_M_toLocaleString; +static bool dyn_arr_proto_unimpl(const char *m) { + static const char *names[] = { "toString", "toLocaleString", NULL }; + for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; + return false; } -static bool dyn_arr_proto_mutates(int m) { - switch (m) { - case DYN_M_push: case DYN_M_pop: case DYN_M_shift: case DYN_M_unshift: case DYN_M_reverse: - case DYN_M_sort: case DYN_M_fill: case DYN_M_copyWithin: case DYN_M_splice: - return true; - default: - return false; - } +static bool dyn_arr_proto_mutates(const char *m) { + static const char *names[] = { + "push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL + }; + for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; + return false; } -static bool dyn_bytes_proto_real(int m) { - switch (m) { - case DYN_M_slice: case DYN_M_at: case DYN_M_indexOf: case DYN_M_lastIndexOf: case DYN_M_includes: - case DYN_M_join: case DYN_M_forEach: case DYN_M_map: case DYN_M_filter: case DYN_M_some: - case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: - case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: - case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: - case DYN_M_toString: case DYN_M_toLocaleString: - return true; - default: - return false; - } +static bool dyn_bytes_proto_real(const char *m) { + static const char *names[] = { "slice", "at", "indexOf", "lastIndexOf", "includes", "join", + "forEach", "map", "filter", "some", "every", "find", "findIndex", "reverse", "fill", "set", + "subarray", "sort", "keys", "values", "entries", "reduce", "reduceRight", "copyWithin", + "toString", "toLocaleString", NULL }; + for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; + return false; } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); /* Live native arrays: the constant-time methods that loops call once per @@ -734,18 +724,18 @@ static ScrDyn *scr_dyn_invoke_impl( * array. Anything that could observe more — a hole, argument coercion, a * multi-value push, a value the element type rejects — sets *handled false * and takes the snapshot path. */ -static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, +static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, bool *handled) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(recv); if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; - if ((mid == DYN_M_push) && argc == 1 && ops->set) { + if (dyn_name_is(method, "push") && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len); } - if ((mid == DYN_M_pop)) { + if (dyn_name_is(method, "pop")) { if (len == 0) { *handled = true; return scr_dyn_retain(scr_dyn_undefined()); @@ -756,7 +746,7 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, *handled = true; return last; } - if ((mid == DYN_M_at) && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { + if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { double index = dyn_index_arg(args, argc, 0, 0, what); if (index < 0) index += (double)len; if (index < 0 || index >= (double)len) { @@ -796,25 +786,11 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); -} - -ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, - int32_t *mid_slot, ScrDyn *const *args, size_t argc, - const char *what) { - if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); - /* The slot is the call site's, shared by every thread: each writes the - * same id. */ - int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); - if (mid < 0) { - mid = dyn_method_of(method); - __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); - } - return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); + return scr_dyn_invoke_impl(recv, method, args, argc, what, NULL); } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; @@ -835,7 +811,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_HANDLE) { return scr_dynh_dispatch(recv, method, args, argc, what); } - if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(mid)) { + if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(method)) { scr_dyn_array_prototype_element_fence(recv); return NULL; } @@ -860,7 +836,7 @@ static ScrDyn *scr_dyn_invoke_impl( * snapshot identity back to the externally visible capsule. */ if (recv->kind == SCR_DYN_TYPED_REF) { bool handled = false; - ScrDyn *fast = dyn_live_array_method(recv, mid, args, argc, what, &handled); + ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); if (handled) return fast; /* Iterator helpers on a compiled generator. */ if (scr_dyn_generator_helper_hook && @@ -885,9 +861,9 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } bool mutates = materialized->kind == SCR_DYN_ARR && - dyn_arr_proto_mutates(mid); + dyn_arr_proto_mutates(method); ScrDyn *result = scr_dyn_invoke_impl( - materialized, method, mid, args, argc, what, recv); + materialized, method, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); if (result == materialized) { scr_dyn_release(result); @@ -916,18 +892,18 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_FUNC) { - if ((mid == DYN_M_apply)) { + if (dyn_name_is(method, "apply")) { return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined(), what); } - if ((mid == DYN_M_call)) { + if (dyn_name_is(method, "call")) { scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined()); ScrDyn *r = scr_dyn_call(recv, argc > 1 ? args + 1 : NULL, argc > 1 ? argc - 1 : 0, what); scr_dyn_this_pop(); return r; } - if ((mid == DYN_M_bind)) return scr_dyn_bind(recv, args, argc); - if ((mid == DYN_M_toString)) { + if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc); + if (dyn_name_is(method, "toString")) { ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method)); if (member && member->kind != SCR_DYN_UNDEF) { scr_dyn_this_push_dyn(recv); @@ -962,9 +938,9 @@ static ScrDyn *scr_dyn_invoke_impl( } if ((recv->kind == SCR_DYN_SYMBOL || recv->kind == SCR_DYN_BIGINT || recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) && - (mid == DYN_M_valueOf)) return scr_dyn_retain(recv); + dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); - if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toFixed)) { + if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toFixed")) { double digits = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; ScrStr *text = scr_num_to_fixed(recv->v.num, digits); @@ -973,7 +949,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(text); return result; } - if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toExponential)) { + if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toExponential")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { dyn_throw_unsupported("Number", method); return NULL; @@ -986,7 +962,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_STR) { ScrStr *s = recv->v.str; - if ((mid == DYN_M_slice)) { + if (dyn_name_is(method, "slice")) { double start = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double end = dyn_index_arg(args, argc, 1, scr_str_utf16_len(s), what); @@ -996,16 +972,16 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if ((mid == DYN_M_at) || (mid == DYN_M_concat) || - (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || - (mid == DYN_M_includes)) { - if (((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || - (mid == DYN_M_includes)) && + if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || + dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || + dyn_name_is(method, "includes")) { + if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || + dyn_name_is(method, "includes")) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - double position = dyn_index_arg(args, argc, 1, (mid == DYN_M_lastIndexOf) ? INFINITY : 0, what); + double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); if (scr_exc_pending()) return NULL; - if ((mid == DYN_M_includes)) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); - if ((mid == DYN_M_indexOf)) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); @@ -1016,19 +992,19 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_ARR) { - if ((mid == DYN_M_keys) || (mid == DYN_M_values) || (mid == DYN_M_entries)) + if (dyn_name_is(method, "keys") || dyn_name_is(method, "values") || dyn_name_is(method, "entries")) return scr_dyn_native_handle_iterator(callback_recv ? callback_recv : recv, - (mid == DYN_M_keys) ? 1 : (mid == DYN_M_entries) ? 2 : 0); + dyn_name_is(method, "keys") ? 1 : dyn_name_is(method, "entries") ? 2 : 0); size_t len = recv->v.arr.len; if (recv->v.arr.presence) { bool sparse = false; for (size_t i = 0; i < len; i++) if (!recv->v.arr.presence[i]) { sparse = true; break; } if (!sparse) { free(recv->v.arr.presence); recv->v.arr.presence = NULL; } - else if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || - (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || - (mid == DYN_M_reduce) || (mid == DYN_M_reduceRight) || (mid == DYN_M_slice) || - (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes)) { + else if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || dyn_name_is(method, "filter") || + dyn_name_is(method, "some") || dyn_name_is(method, "every") || dyn_name_is(method, "find") || + dyn_name_is(method, "findIndex") || dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex") || + dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight") || dyn_name_is(method, "slice") || + dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { ScrDyn *pack = scr_dyn_new_arr(); for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(pack, scr_dyn_retain(args[i])); ScrStr *name = scr_str_new(method, strlen(method)); @@ -1036,7 +1012,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(name); scr_dyn_release(pack); return result; - } else if (dyn_arr_proto_mutates(mid) && !(mid == DYN_M_fill) && !(mid == DYN_M_push) && !(mid == DYN_M_pop) && !(mid == DYN_M_shift)) { + } else if (dyn_arr_proto_mutates(method) && !dyn_name_is(method, "fill") && !dyn_name_is(method, "push") && !dyn_name_is(method, "pop") && !dyn_name_is(method, "shift")) { static const char message[] = "This checked array mutation over holes is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -1044,11 +1020,11 @@ static ScrDyn *scr_dyn_invoke_impl( } size_t splice_start = 0; size_t splice_removed = 0; - if (dyn_arr_proto_mutates(mid)) { - bool grows = (mid == DYN_M_push) && argc > 0; - grows = grows || ((mid == DYN_M_unshift) && argc > 0); - bool shrinks = ((mid == DYN_M_pop) || (mid == DYN_M_shift)) && len > 0; - if ((mid == DYN_M_splice)) { + if (dyn_arr_proto_mutates(method)) { + bool grows = dyn_name_is(method, "push") && argc > 0; + grows = grows || (dyn_name_is(method, "unshift") && argc > 0); + bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0; + if (dyn_name_is(method, "splice")) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; splice_start = dyn_rel_index(startD, len); @@ -1085,33 +1061,33 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && ((mid == DYN_M_push) || - (mid == DYN_M_pop) || (mid == DYN_M_shift) || - (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { + if (recv->v.arr.frozen && (dyn_name_is(method, "push") || + dyn_name_is(method, "pop") || dyn_name_is(method, "shift") || + dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) { static const char message[] = "Cannot assign to read only property 'length'"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || - (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || - ((mid == DYN_M_reverse) && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) || + (recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) || + (dyn_name_is(method, "reverse") && len > 1)))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } } - if ((mid == DYN_M_push)) { + if (dyn_name_is(method, "push")) { for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); return scr_dyn_new_num((double)recv->v.arr.len); } - if ((mid == DYN_M_pop)) { + if (dyn_name_is(method, "pop")) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *result = scr_dyn_arr_at(recv, (double)(len - 1)); if (!result) return NULL; scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); return result; } - if ((mid == DYN_M_shift)) { + if (dyn_name_is(method, "shift")) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); if (recv->v.arr.sealed) { if (recv->v.arr.frozen) { @@ -1133,7 +1109,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = len - 1; return first; /* ownership moves out */ } - if ((mid == DYN_M_unshift)) { + if (dyn_name_is(method, "unshift")) { /* Grow the backing store before shifting the old elements. The * appended placeholders are immortal, so overwriting them does not * abandon retained argument references. */ @@ -1145,7 +1121,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_new_num((double)recv->v.arr.len); } - if ((mid == DYN_M_splice)) { + if (dyn_name_is(method, "splice")) { size_t start = splice_start; size_t removed = splice_removed; size_t inserted = argc > 2 ? argc - 2 : 0; @@ -1175,7 +1151,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = next_len; return out; } - if ((mid == DYN_M_slice)) { + if (dyn_name_is(method, "slice")) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 1, (double)len, what); @@ -1187,7 +1163,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start; i < end; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i])); return out; } - if ((mid == DYN_M_at)) { + if (dyn_name_is(method, "at")) { double iD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1195,17 +1171,17 @@ static ScrDyn *scr_dyn_invoke_impl( if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_arr_at(recv, idx); } - if ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || - (mid == DYN_M_includes)) { - if (len == 0) return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || + dyn_name_is(method, "includes")) { + if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); - double from = (mid == DYN_M_lastIndexOf) ? (double)len - 1 : 0; + double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0; if (argc > 1) { from = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; } if (!dyn_arr_length_unchanged(recv, len)) return NULL; - if ((mid == DYN_M_lastIndexOf)) { + if (dyn_name_is(method, "lastIndexOf")) { double first = from < 0 ? (double)len + from : from; if (first >= (double)len) first = (double)len - 1; for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) { @@ -1217,16 +1193,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (first < 0) first = 0; for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) { const ScrDyn *item = recv->v.arr.items[i]; - bool nan_match = (mid == DYN_M_includes) && + bool nan_match = dyn_name_is(method, "includes") && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM && item->v.num != item->v.num && needle->v.num != needle->v.num; if (nan_match || scr_dyn_strict_eq(item, needle)) { - return (mid == DYN_M_includes) ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return dyn_name_is(method, "includes") ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); } } - return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { + if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t i = len; i > 0; i--) { @@ -1239,16 +1215,16 @@ static ScrDyn *scr_dyn_invoke_impl( bool matched = scr_dyn_truthy(result); scr_dyn_release(result); if (matched) { - if ((mid == DYN_M_findLast)) return item; + if (dyn_name_is(method, "findLast")) return item; scr_dyn_release(item); return scr_dyn_new_num((double)index); } scr_dyn_release(item); } - return (mid == DYN_M_findLast) + return dyn_name_is(method, "findLast") ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_join)) { + if (dyn_name_is(method, "join")) { ScrJsonBuf b; scr_jb_init(&b); ScrStr *separator = argc > 0 && args[0]->kind != SCR_DYN_UNDEF @@ -1274,7 +1250,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return r; } - if ((mid == DYN_M_concat)) { + if (dyn_name_is(method, "concat")) { ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { if (!dyn_arr_copy_item(out, recv, i, true)) { scr_dyn_release(out); return NULL; } @@ -1295,7 +1271,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_flat)) { + if (dyn_name_is(method, "flat")) { double depth = dyn_index_arg(args, argc, 0, 1, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1307,7 +1283,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_reverse)) { + if (dyn_name_is(method, "reverse")) { for (size_t i = 0; i < len / 2; i++) { ScrDyn *tmp = recv->v.arr.items[i]; recv->v.arr.items[i] = recv->v.arr.items[len - 1 - i]; @@ -1315,12 +1291,12 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_toReversed)) { + if (dyn_name_is(method, "toReversed")) { ScrDyn *out = scr_dyn_new_arr(); if (!dyn_arr_reversed_copy(out, recv)) { scr_dyn_release(out); return NULL; } return out; } - if ((mid == DYN_M_with)) { + if (dyn_name_is(method, "with")) { double indexD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1338,7 +1314,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_toSpliced)) { + if (dyn_name_is(method, "toSpliced")) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; size_t start = dyn_rel_index(startD, len); @@ -1355,7 +1331,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start + delete_count; i < len; i++) if (!dyn_arr_copy_item(out, recv, i, false)) { scr_dyn_release(out); return NULL; } return out; } - if ((mid == DYN_M_fill)) { + if (dyn_name_is(method, "fill")) { double startD = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 2, (double)len, what); @@ -1383,7 +1359,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_copyWithin)) { + if (dyn_name_is(method, "copyWithin")) { double targetD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double startD = dyn_index_arg(args, argc, 1, 0, what); @@ -1416,13 +1392,13 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || - (mid == DYN_M_filter) || (mid == DYN_M_some) || - (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex)) { + if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || + dyn_name_is(method, "filter") || dyn_name_is(method, "some") || + dyn_name_is(method, "every") || dyn_name_is(method, "find") || + dyn_name_is(method, "findIndex")) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0]; - ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; + ScrDyn *out = (dyn_name_is(method, "map") || dyn_name_is(method, "filter")) ? scr_dyn_new_arr() : NULL; /* Array iteration methods snapshot length before the first callback. * A shrinking callback can remove a later dense entry; an expanding * callback must not make the new tail observable to this pass. Live @@ -1437,30 +1413,30 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if ((mid == DYN_M_map)) { + if (dyn_name_is(method, "map")) { scr_dyn_arr_push(out, r); /* ownership moves in */ r = NULL; } else { bool truthy = scr_dyn_truthy(r); scr_dyn_release(r); - if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ - if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + if (dyn_name_is(method, "filter") && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if (dyn_name_is(method, "some") && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if (dyn_name_is(method, "every") && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if (dyn_name_is(method, "find") && truthy) return item; /* +1 moves out */ + if (dyn_name_is(method, "findIndex") && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } } scr_dyn_release(item); } if (out) return out; - if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); - if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); - if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); - if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); + if (dyn_name_is(method, "some")) return scr_dyn_new_bool(false); + if (dyn_name_is(method, "every")) return scr_dyn_new_bool(true); + if (dyn_name_is(method, "find")) return scr_dyn_retain(scr_dyn_undefined()); + if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1); return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ } - if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { + if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) { if (!dyn_cb_check(args, argc)) return NULL; - bool backward = (mid == DYN_M_reduceRight); + bool backward = dyn_name_is(method, "reduceRight"); ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t step = 0; step < len; step++) { @@ -1482,7 +1458,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((mid == DYN_M_flatMap)) { + if (dyn_name_is(method, "flatMap")) { /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 * flatten. Native dyn-array results flatten element-by-element; a * WRAPPED engine array flattens through the routed keyed reads @@ -1520,7 +1496,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if ((mid == DYN_M_sort)) { + if (dyn_name_is(method, "sort")) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { /* V8 appends the received value's string image. */ @@ -1534,7 +1510,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL; return scr_dyn_retain(recv); } - if ((mid == DYN_M_toSorted)) { + if (dyn_name_is(method, "toSorted")) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { static const char message[] = "The comparison function must be either a function or undefined"; @@ -1549,7 +1525,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_arr_proto_unimpl(mid)) { + if (dyn_arr_proto_unimpl(method)) { dyn_throw_unsupported("Array", method); return NULL; } @@ -1565,14 +1541,14 @@ static ScrDyn *scr_dyn_invoke_impl( #ifdef SCR_LIB scr_trap("scriptc: promise dispatch is unavailable in library mode\n"); #else - if ((mid == DYN_M_then)) { + if (dyn_name_is(method, "then")) { return scr_dyn_promise_then(recv->v.promise, argc >= 1 ? args[0] : NULL, argc >= 2 ? args[1] : NULL, NULL); } - if ((mid == DYN_M_catch)) { + if (dyn_name_is(method, "catch")) { return scr_dyn_promise_then(recv->v.promise, NULL, argc >= 1 ? args[0] : NULL, NULL); } - if ((mid == DYN_M_finally)) { + if (dyn_name_is(method, "finally")) { return scr_dyn_promise_then(recv->v.promise, NULL, NULL, argc >= 1 ? args[0] : NULL); } dyn_throw_not_fn(what); @@ -1624,44 +1600,44 @@ static ScrDyn *scr_dyn_invoke_impl( } /* The typed array methods below are not DataView's; it inherits * Object.prototype's. */ - if ((mid == DYN_M_toString) || (mid == DYN_M_toLocaleString)) { + if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { ScrStr *tag = scr_str_new("[object DataView]", 17); ScrDyn *out = scr_dyn_new_str(tag); scr_str_release(tag); return out; } - if ((mid == DYN_M_valueOf)) return scr_dyn_retain(recv); + if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); dyn_throw_not_fn(what); return NULL; } size_t blen = bytes->len; - if ((mid == DYN_M_at)) { + if (dyn_name_is(method, "at")) { double iD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double idx = iD < 0 ? (double)blen + iD : iD; if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_new_num(scr_bytes_get(bytes, idx)); } - if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { + if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { /* subarray and Buffer.slice alias their source. TypedArray.slice * owns an independent copy; all keep the receiver's Buffer flavor. */ double startD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) + ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - if ((mid == DYN_M_set)) { + if (dyn_name_is(method, "set")) { double offset = dyn_bytes_index_arg(args, argc, 1, 0); if (scr_exc_pending()) return NULL; scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if ((mid == DYN_M_copyWithin)) { + if (dyn_name_is(method, "copyWithin")) { double target = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1671,7 +1647,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); return scr_dyn_retain(recv); } - if ((mid == DYN_M_fill) && !recv->buffer) { + if (dyn_name_is(method, "fill") && !recv->buffer) { double value; if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1681,8 +1657,8 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); return scr_dyn_retain(recv); } - if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { - ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF + if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) { + ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); if (scr_exc_pending()) { scr_str_release(separator); return NULL; } ScrStr *joined = scr_bytes_join(bytes, separator); @@ -1691,7 +1667,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } - if (dyn_bytes_proto_real(mid)) { + if (dyn_bytes_proto_real(method)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; } diff --git a/packages/runtime/src/scr_dyn_methods.h b/packages/runtime/src/scr_dyn_methods.h deleted file mode 100644 index 49e5c7254..000000000 --- a/packages/runtime/src/scr_dyn_methods.h +++ /dev/null @@ -1,160 +0,0 @@ -/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c - * uses. Do not edit by hand. */ -#ifndef SCR_DYN_METHODS_H -#define SCR_DYN_METHODS_H - -#include -#include - -enum { - DYN_M_NONE, - DYN_M_apply, - DYN_M_at, - DYN_M_bind, - DYN_M_call, - DYN_M_catch, - DYN_M_concat, - DYN_M_copyWithin, - DYN_M_entries, - DYN_M_every, - DYN_M_fill, - DYN_M_filter, - DYN_M_finally, - DYN_M_find, - DYN_M_findIndex, - DYN_M_findLast, - DYN_M_findLastIndex, - DYN_M_flat, - DYN_M_flatMap, - DYN_M_forEach, - DYN_M_includes, - DYN_M_indexOf, - DYN_M_join, - DYN_M_keys, - DYN_M_lastIndexOf, - DYN_M_map, - DYN_M_pop, - DYN_M_push, - DYN_M_reduce, - DYN_M_reduceRight, - DYN_M_reverse, - DYN_M_set, - DYN_M_shift, - DYN_M_slice, - DYN_M_some, - DYN_M_sort, - DYN_M_splice, - DYN_M_subarray, - DYN_M_then, - DYN_M_toExponential, - DYN_M_toFixed, - DYN_M_toLocaleString, - DYN_M_toReversed, - DYN_M_toSorted, - DYN_M_toSpliced, - DYN_M_toString, - DYN_M_unshift, - DYN_M_valueOf, - DYN_M_values, - DYN_M_with, - DYN_M_COUNT -}; - -static const char *const dyn_method_names[DYN_M_COUNT] = { - NULL, - "apply", - "at", - "bind", - "call", - "catch", - "concat", - "copyWithin", - "entries", - "every", - "fill", - "filter", - "finally", - "find", - "findIndex", - "findLast", - "findLastIndex", - "flat", - "flatMap", - "forEach", - "includes", - "indexOf", - "join", - "keys", - "lastIndexOf", - "map", - "pop", - "push", - "reduce", - "reduceRight", - "reverse", - "set", - "shift", - "slice", - "some", - "sort", - "splice", - "subarray", - "then", - "toExponential", - "toFixed", - "toLocaleString", - "toReversed", - "toSorted", - "toSpliced", - "toString", - "unshift", - "valueOf", - "values", - "with", -}; - -static const unsigned char dyn_method_lengths[DYN_M_COUNT] = { - 0, 5, 2, 4, 4, 5, 6, 10, 7, 5, 4, 6, 7, 4, 9, 8, - 13, 4, 7, 7, 8, 7, 4, 4, 11, 3, 3, 4, 6, 11, 7, 3, - 5, 5, 4, 4, 6, 8, 4, 13, 7, 14, 10, 8, 9, 8, 7, 7, - 6, 4, -}; - -/* Each name's id at its slot (or the next free one); 0 ends a probe. */ -static const unsigned char dyn_method_slots[256] = { - 0, 0, 0, 0, 12, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 14, 0, 0, 27, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 28, 3, 0, - 22, 0, 20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, - 0, 0, 0, 0, 0, 33, 31, 0, 0, 0, 0, 0, 36, 35, 0, 0, - 0, 0, 0, 0, 23, 4, 32, 0, 5, 0, 0, 0, 0, 0, 0, 29, - 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 6, 0, 0, 37, 0, - 0, 40, 0, 0, 0, 0, 0, 0, 43, 0, 0, 45, 0, 0, 0, 44, - 0, 7, 0, 0, 0, 0, 42, 0, 25, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, - 46, 0, 0, 39, 0, 41, 0, 9, 0, 0, 13, 0, 0, 0, 0, 8, - 0, 47, 10, 0, 0, 0, 0, 48, 0, 0, 17, 0, 0, 49, 0, 0, - 0, 0, 0, 19, 0, 0, 11, 0, 0, 0, 0, 18, 0, 0, 0, 0, -}; - -#define DYN_METHOD_MAX_LENGTH 14 - -/* The id of a method name, compared by content; DYN_M_NONE for any other - * name. */ -static inline int dyn_method_of(const char *m) { - size_t length = strlen(m); - if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE; - unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 + - (unsigned char)m[length - 1]) & 255; - for (;;) { - unsigned id = dyn_method_slots[slot]; - if (id == 0) return DYN_M_NONE; - if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id; - slot = (slot + 1) & 255; - } -} - -#endif diff --git a/packages/runtime/src/scr_dyn_methods.test.ts b/packages/runtime/src/scr_dyn_methods.test.ts deleted file mode 100644 index 77911aa2d..000000000 --- a/packages/runtime/src/scr_dyn_methods.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { join } from "node:path"; -import { expect, test } from "vitest"; - -test("scr_dyn_methods.h lists every method id the checked-dynamic dispatch compares", () => { - const script = join(import.meta.dirname, "../../../scripts/gen-dyn-methods.mjs"); - expect(() => - execFileSync(process.execPath, [script, "--check"], { stdio: "pipe" }), - ).not.toThrow(); -}); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index a23395c91..3d10183f4 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -354,23 +354,17 @@ static bool scr_worker_transfer_has(const ScrDyn *transfer, const ScrWorkerView return false; } -/* Whether a method name is `name`: first characters compare inline, so - * the dispatch ladders below call strcmp only for names that can match. */ -static inline bool method_is(const char *method, const char *name) { - return method[0] == name[0] && strcmp(method, name) == 0; -} - static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (method_is(method, "ref") || method_is(method, "unref")) { - view->referenced = method_is(method, "ref"); + if (!strcmp(method, "ref") || !strcmp(method, "unref")) { + view->referenced = !strcmp(method, "ref"); return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && method_is(method, "hasRef")) + if (view->port && !strcmp(method, "hasRef")) return scr_dyn_new_bool(view->referenced && (!view->closed || (view->channel && view->closing))); - if (!view->port && method_is(method, "terminate")) { + if (!view->port && !strcmp(method, "terminate")) { if (!view->termination) { view->termination = scr_promise_new(); if (view->closed) scr_promise_fulfill_ref(view->termination, @@ -379,7 +373,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_new_promise(view->termination); } - if (method_is(method, "postMessage")) { + if (!strcmp(method, "postMessage")) { /* Worker drops messages after exit before validating or cloning. A * closed MessagePort still validates its payload, as Node does. */ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined()); @@ -411,9 +405,9 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho else scr_worker_thread_post(view->thread, message); return scr_dyn_retain(scr_dyn_undefined()); } - bool once = method_is(method, "once") || method_is(method, "prependOnceListener"); - bool prepend = method_is(method, "prependListener") || method_is(method, "prependOnceListener"); - if (once || prepend || method_is(method, "on") || method_is(method, "addListener")) { + bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener"); + bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener"); + if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -422,7 +416,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (method_is(method, "off") || method_is(method, "removeListener")) { + if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -431,21 +425,21 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (method_is(method, "removeAllListeners")) { + if (!strcmp(method, "removeAllListeners")) { bool all = !argc || args[0]->kind == SCR_DYN_UNDEF; if (!all && args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (method_is(method, "listenerCount")) { + if (!strcmp(method, "listenerCount")) { if (!argc || args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1]) : scr_emitter_listener_count(view->events, args[0]->v.str)); } - if (view->port && method_is(method, "close")) { + if (view->port && !strcmp(method, "close")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { scr_dyn_check_listener(args[0], "callback"); if (scr_exc_pending()) return NULL; @@ -466,7 +460,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && method_is(method, "start")) { + if (view->port && !strcmp(method, "start")) { view->started = true; return scr_dyn_retain(scr_dyn_undefined()); } @@ -1027,7 +1021,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (method_is(method, "postMessage")) { + if (!strcmp(method, "postMessage")) { if (view->closed) { scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); return NULL; @@ -1051,7 +1045,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, scr_message_free(first); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (method_is(method, "close")) { + if (!strcmp(method, "close")) { if (!view->closed) { view->closed = true; scr_broadcast_leave(view->channel); @@ -1059,8 +1053,8 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (method_is(method, "ref") || method_is(method, "unref")) { - view->referenced = method_is(method, "ref"); + if (!strcmp(method, "ref") || !strcmp(method, "unref")) { + view->referenced = !strcmp(method, "ref"); return scr_dyn_retain(self); } ScrDyn *out = NULL; diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs deleted file mode 100644 index fe4f81a90..000000000 --- a/scripts/gen-dyn-methods.mjs +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env node -/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the - * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the - * names, and an open-addressed hash table mapping a name to its id. The ids - * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the - * dispatch code. - * - * node scripts/gen-dyn-methods.mjs write the header - * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ -import { readFileSync, writeFileSync } from "node:fs"; - -const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); -const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); -const SLOTS = 256; - -const used = new Set(); -for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) - if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); -const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); -if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); - -// The slot dyn_method_of in the header computes. -const slotOf = (name) => - (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); -const slots = new Array(SLOTS).fill(0); -names.forEach((name, i) => { - let slot = slotOf(name); - while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); - slots[slot] = i + 1; -}); - -const rows = (values, width) => { - const out = []; - for (let i = 0; i < values.length; i += width) - out.push(` ${values.slice(i, i + width).join(", ")},`); - return out; -}; -const text = [ - "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", - " * uses. Do not edit by hand. */", - "#ifndef SCR_DYN_METHODS_H", - "#define SCR_DYN_METHODS_H", - "", - "#include ", - "#include ", - "", - "enum {", - " DYN_M_NONE,", - ...names.map((name) => ` DYN_M_${name},`), - " DYN_M_COUNT", - "};", - "", - "static const char *const dyn_method_names[DYN_M_COUNT] = {", - " NULL,", - ...names.map((name) => ` ${JSON.stringify(name)},`), - "};", - "", - "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", - ...rows([0, ...names.map((name) => name.length)], 16), - "};", - "", - `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, - `static const unsigned char dyn_method_slots[${SLOTS}] = {`, - ...rows(slots, 16), - "};", - "", - `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, - "", - "/* The id of a method name, compared by content; DYN_M_NONE for any other", - " * name. */", - "static inline int dyn_method_of(const char *m) {", - " size_t length = strlen(m);", - " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", - " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", - ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, - " for (;;) {", - " unsigned id = dyn_method_slots[slot];", - " if (id == 0) return DYN_M_NONE;", - " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", - ` slot = (slot + 1) & ${SLOTS - 1};`, - " }", - "}", - "", - "#endif", - "", -].join("\n"); - -if (process.argv.includes("--check")) { - let current = ""; - try { - current = readFileSync(target, "utf8"); - } catch {} - if (current !== text) { - console.error( - "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", - ); - process.exit(1); - } -} else { - writeFileSync(target, text); -} diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 160e33438..3158de997 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,8 +394,7 @@ 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. 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. */ @@ -409,7 +408,6 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", - "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c4013d288..c0176eb93 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" }, + runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From f769a973df5f5275b827f92a595860098cd17965 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 16:04:38 -0700 Subject: [PATCH 054/169] Map an interface that extends an any-typed base to a checked-dynamic value In a compiled program an opted-in package is typed from its JavaScript, so a type computed from members only its declarations have (zod's z.infer) is any. An interface extending it mapped to a record of its own declared fields, and reads of inherited fields and spreads into it were refused. It is now checked-dynamic, like the base. The zod3-infer npm case uses z.infer, z.input and z.output across objects, interfaces extending inferred types, spreads, optional and nullable fields, unions, discriminated unions, transforms, defaults, .extend and arrays. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.ts | 23 + tests/fixtures/npm/cases/zod3-infer/main.ts | 118 + .../cases/zod3-infer/node_modules/zod/LICENSE | 21 + .../node_modules/zod/dist/esm/index.js | 3 + .../node_modules/zod/dist/esm/package.json | 3 + .../node_modules/zod/dist/esm/v3/ZodError.js | 132 + .../node_modules/zod/dist/esm/v3/errors.js | 9 + .../node_modules/zod/dist/esm/v3/external.js | 6 + .../zod/dist/esm/v3/helpers/enumUtil.js | 1 + .../zod/dist/esm/v3/helpers/errorUtil.js | 6 + .../zod/dist/esm/v3/helpers/parseUtil.js | 109 + .../zod/dist/esm/v3/helpers/partialUtil.js | 1 + .../zod/dist/esm/v3/helpers/typeAliases.js | 1 + .../zod/dist/esm/v3/helpers/util.js | 133 + .../node_modules/zod/dist/esm/v3/index.js | 4 + .../zod/dist/esm/v3/locales/en.js | 107 + .../zod/dist/esm/v3/standard-schema.js | 1 + .../node_modules/zod/dist/esm/v3/types.js | 3691 +++++++++++++++++ .../node_modules/zod/dist/types/index.d.ts | 3 + .../zod/dist/types/v3/ZodError.d.ts | 164 + .../zod/dist/types/v3/errors.d.ts | 5 + .../zod/dist/types/v3/external.d.ts | 6 + .../zod/dist/types/v3/helpers/enumUtil.d.ts | 8 + .../zod/dist/types/v3/helpers/errorUtil.d.ts | 9 + .../zod/dist/types/v3/helpers/parseUtil.d.ts | 78 + .../dist/types/v3/helpers/partialUtil.d.ts | 8 + .../dist/types/v3/helpers/typeAliases.d.ts | 2 + .../zod/dist/types/v3/helpers/util.d.ts | 85 + .../node_modules/zod/dist/types/v3/index.d.ts | 4 + .../zod/dist/types/v3/locales/en.d.ts | 3 + .../zod/dist/types/v3/standard-schema.d.ts | 102 + .../node_modules/zod/dist/types/v3/types.d.ts | 1031 +++++ .../zod3-infer/node_modules/zod/package.json | 120 + .../npm/cases/zod3-infer/package.json | 1 + 34 files changed, 5998 insertions(+) create mode 100644 tests/fixtures/npm/cases/zod3-infer/main.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/index.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-infer/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/cases/zod3-infer/package.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index bd2a32c37..e56110687 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -7592,6 +7592,22 @@ function wideningAddedProperties( return added; } +/** Whether `type` is an interface one of whose `extends` clauses names a + * type the checker answers `any` (or an error type) for. */ +function interfaceExtendsAny(type: ts.Type, checker: ts.TypeChecker): boolean { + const symbol = type.getSymbol(); + if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Interface) === 0) return false; + for (const decl of symbol.declarations) { + if (!ts.isInterfaceDeclaration(decl)) continue; + for (const clause of decl.heritageClauses?.nodes ?? []) { + for (const base of clause.types.nodes) { + if ((checker.getTypeAtLocation(base).flags & ts.TypeFlags.Any) !== 0) return true; + } + } + } + return false; +} + function mapRecordTypeInner( widened: ts.Type, ctx: TypeMapperCtx, @@ -7607,6 +7623,13 @@ function mapRecordTypeInner( // names the construct. const widenedSym = widened.getSymbol(); if (widenedSym !== undefined && (widenedSym.flags & ts.SymbolFlags.Enum) !== 0) return null; + // An interface extending a type the checker answers `any` for (in the + // compiled world, a package type computed from members only its .d.ts + // declares, such as `interface P extends z.infer`) has members + // the checker cannot list: its own declared fields are not the whole + // object. The value is checked-dynamic, never a record of the declared + // fields alone. + if (interfaceExtendsAny(widened, checker)) return DYN; // An index signature maps to a hybrid shape: declared fields keep // struct slots, undeclared keys ride the shape's overflow map, valued // uniformly by the signature's value type (`unknown` → dyn — a model-pricing table's diff --git a/tests/fixtures/npm/cases/zod3-infer/main.ts b/tests/fixtures/npm/cases/zod3-infer/main.ts new file mode 100644 index 000000000..49029c38e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/main.ts @@ -0,0 +1,118 @@ +// Types derived from zod 3.25 schemas (z.infer, z.input, z.output) used as +// ordinary typed data: field reads, spreads, interfaces extending inferred +// types, unions and discriminated unions narrowed by their tags, arrays, +// optional and nullable fields, transforms and defaults, and .extend. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Objects, and interfaces that extend an inferred type. +const ProductInput = z.object({ + name: z.string().min(1), + priceCents: z.number().int().nonnegative(), + tags: z.array(z.string()).default([]), +}); +type ProductInput = z.infer; +interface Product extends ProductInput { + id: string; +} + +function makeProduct(id: string, input: ProductInput): Product { + return { id, ...input }; +} +const parsed = ProductInput.parse({ name: "Keyboard", priceCents: 12900, tags: ["input"] }); +const product = makeProduct("p1", parsed); +show("product", product); +console.log("name", product.name, "price", product.priceCents, "tags", product.tags.join("|")); +const products: Product[] = [product, makeProduct("p2", ProductInput.parse({ name: "Cable", priceCents: 900 }))]; +console.log("tagged", products.filter((p) => p.tags.includes("input")).map((p) => p.name).join(",")); +let total = 0; +for (const p of products) total += p.priceCents; +console.log("total", total); + +// safeParse data spread into a record with more fields. +const OrderInput = z.object({ + productId: z.string(), + quantity: z.coerce.number().int().min(1), + email: z.string().email(), +}); +type OrderInput = z.infer; +interface Order extends OrderInput { + id: string; + totalCents: number; +} +function placeOrder(raw: unknown): Order | string { + const result = OrderInput.safeParse(raw); + if (!result.success) return result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "); + return { id: "o1", ...result.data, totalCents: result.data.quantity * 4900 }; +} +show("order", placeOrder({ productId: "p2", quantity: "3", email: "a@b.co" })); +show("bad order", placeOrder({ productId: "p2", quantity: "0", email: "nope" })); + +// Optional and nullable fields. +const Profile = z.object({ + handle: z.string(), + bio: z.string().optional(), + avatar: z.string().nullable(), + age: z.number().optional().nullable(), +}); +type Profile = z.infer; +function describe(p: Profile): string { + const bio = p.bio ?? "(no bio)"; + const avatar = p.avatar === null ? "(no avatar)" : p.avatar; + const age = p.age == null ? "?" : String(p.age); + return `${p.handle}: ${bio}, ${avatar}, ${age}`; +} +console.log(describe(Profile.parse({ handle: "ada", avatar: null }))); +console.log(describe(Profile.parse({ handle: "bob", bio: "hi", avatar: "b.png", age: 42 }))); + +// Unions and discriminated unions narrowed by their tag. +const Shape = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("circle"), radius: z.number() }), + z.object({ kind: z.literal("rect"), width: z.number(), height: z.number() }), +]); +type Shape = z.infer; +function area(s: Shape): number { + switch (s.kind) { + case "circle": + return Math.round(Math.PI * s.radius * s.radius); + case "rect": + return s.width * s.height; + } +} +const shapes: Shape[] = z.array(Shape).parse([ + { kind: "circle", radius: 2 }, + { kind: "rect", width: 3, height: 4 }, +]); +console.log("areas", shapes.map(area).join(",")); + +const IdOrName = z.union([z.number(), z.string()]); +type IdOrName = z.infer; +const label = (v: IdOrName): string => (typeof v === "number" ? `#${v}` : v.toUpperCase()); +console.log("labels", [IdOrName.parse(7), IdOrName.parse("x")].map(label).join(",")); + +// Transforms and defaults: input and output types differ. +const Settings = z.object({ + port: z.string().transform((s) => Number(s)), + mode: z.enum(["dev", "prod"]).default("dev"), +}); +type SettingsIn = z.input; +type SettingsOut = z.output; +const rawSettings: SettingsIn = { port: "8080" }; +const settings: SettingsOut = Settings.parse(rawSettings); +console.log("settings", settings.port + 1, settings.mode, typeof settings.port, rawSettings.port.length); + +// .extend and arrays of inferred records. +const Base = z.object({ id: z.string(), createdAt: z.string() }); +const User = Base.extend({ email: z.string().email(), roles: z.array(z.enum(["admin", "user"])) }); +type User = z.infer; +const users: User[] = z.array(User).parse([ + { id: "u1", createdAt: "2026-01-01", email: "a@b.co", roles: ["admin", "user"] }, + { id: "u2", createdAt: "2026-01-02", email: "c@d.co", roles: ["user"] }, +]); +const admins = users.filter((u) => u.roles.includes("admin")).map((u) => `${u.id}<${u.email}>`); +console.log("admins", admins.join(",")); +const renamed = users.map((u) => ({ ...u, id: u.id.toUpperCase() })); +show("renamed", renamed); diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/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/cases/zod3-infer/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @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. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * 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) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * 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(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +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 const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +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"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + 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/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -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/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -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/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +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/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +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/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -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/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -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/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -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/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +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> { + private _cached; + _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/cases/zod3-infer/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-infer/package.json b/tests/fixtures/npm/cases/zod3-infer/package.json new file mode 100644 index 000000000..ccfb8101b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-infer/package.json @@ -0,0 +1 @@ +{ "name": "zod3-infer-case", "private": true, "type": "module" } From 067be1568324673ad768423fea5987a432697db5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:12:38 -0500 Subject: [PATCH 055/169] Write and hash the LLVM module text in slices instead of whole copies The native compiler wrote the module's LLVM text, 150 MB on a large program such as tsc-ts, by encoding the whole string at once; its frontend cache entry by first escaping the whole text into a second string and then encoding that; and it read the written file back whole to key the object cache. Those copies were the build's peak. Text parts are now encoded and written in slices of at most 1 MiB UTF-16 units that never split a surrogate pair. The frontend cache entry is generated while it is written: the LLVM text is escaped slice by slice, which is the same JSON as escaping it whole, the entry is hashed in the same pass, and an entry past one native string abandons its write as it was skipped before. The object cache key hashes the file in 1 MiB blocks. The files and cache keys are unchanged. --- packages/compiler/src/native/cache.test.ts | 18 +++++- packages/compiler/src/native/cache.ts | 28 +++++++-- packages/compiler/src/native/compiler.ts | 14 +++-- packages/compiler/src/native/prepare.ts | 48 +++++++++----- .../compiler/src/native/text-file.test.ts | 42 +++++++++++++ packages/compiler/src/native/text-file.ts | 62 ++++++++++++++++--- 6 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/src/native/text-file.test.ts diff --git a/packages/compiler/src/native/cache.test.ts b/packages/compiler/src/native/cache.test.ts index 3ffdf2335..f4910ff23 100644 --- a/packages/compiler/src/native/cache.test.ts +++ b/packages/compiler/src/native/cache.test.ts @@ -11,7 +11,7 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; -import { contentDigest, NativeCache, openNativeCache } from "./cache.js"; +import { contentDigest, fileDigest, NativeCache, openNativeCache } from "./cache.js"; const directories: string[] = []; function directory(): string { @@ -19,11 +19,24 @@ function directory(): string { directories.push(path); return path; } +function* generate(parts: readonly string[]): Generator { + yield* parts; +} afterEach(() => { vi.unstubAllEnvs(); for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); }); +test("a file's digest read in blocks is the digest of its bytes", () => { + const path = join(directory(), "module.ll"); + const bytes = Buffer.alloc(3 * 1024 * 1024 + 17); + for (let index = 0; index < bytes.length; index++) bytes[index] = (index * 31) & 0xff; + writeFileSync(path, bytes); + expect(fileDigest(path)).toBe(contentDigest(bytes)); + writeFileSync(path, ""); + expect(fileDigest(path)).toBe(contentDigest("")); +}); + test("cache hits require an intact payload and digest", () => { const cache = new NativeCache(directory()); const key = contentDigest("program identity"); @@ -42,7 +55,8 @@ test("text parts publish one digest-checked entry and oversized reads are misses const cache = new NativeCache(directory()); const key = contentDigest("parts identity"); const parts = ['{"a":', '"\u00e9é😀"', ",", "", '"b":1}']; - cache.writeParts("frontend", key, parts); + // Parts are generated while they are written, and hashed in the same pass. + cache.writeParts("frontend", key, generate(parts)); const bytes = cache.read("frontend", key); expect(bytes?.toString("utf8")).toBe(parts.join("")); expect(readFileSync(join(cache.root, "frontend", key + ".sha256"), "utf8").trim()).toBe( diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index f81311f52..566df308a 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -1,8 +1,11 @@ import { createHash } from "node:crypto"; import { + closeSync, mkdirSync, mkdtempSync, + openSync, readFileSync, + readSync, readdirSync, renameSync, rmSync, @@ -11,7 +14,7 @@ import { } from "node:fs"; import { join } from "node:path"; import { resolveBuildCacheRoot } from "../backend/cache-root.js"; -import { writeTextPartsSync } from "./text-file.js"; +import { writeTextGeneratedSync } from "./text-file.js"; /** UTF-8 bytes one native compiler string can hold (the runtime's * SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads @@ -25,6 +28,23 @@ export function contentDigest(value: string | Uint8Array): string { return hash.digest("hex"); } +/** contentDigest of a file's bytes, read in 1 MiB blocks so a large file + * (a module's LLVM text) is never held whole. */ +export function fileDigest(path: string): string { + const hash = createHash("sha256"); + const block = Buffer.alloc(1024 * 1024); + const fd = openSync(path, "r"); + try { + for (let read = readSync(fd, block, 0, block.length, null); read > 0;) { + hash.update(block.subarray(0, read)); + read = readSync(fd, block, 0, block.length, null); + } + } finally { + closeSync(fd); + } + return hash.digest("hex"); +} + /** A compiler cache contains private source and executable data. The host * checks ownership and permissions before this class accepts a root. */ export class NativeCache { @@ -86,15 +106,15 @@ export class NativeCache { } /** Publish text parts without joining them into one string. */ - writeParts(family: string, key: string, parts: readonly string[]): void { + writeParts(family: string, key: string, parts: Generator): void { let stage: string | null = null; try { const directory = join(this.root, family); mkdirSync(directory, { recursive: true, mode: 0o700 }); stage = mkdtempSync(join(directory, ".write-")); - writeTextPartsSync(join(stage, "payload"), parts); + // One pass over the parts: they may be generated while they are written. const hash = createHash("sha256"); - for (const part of parts) hash.update(part); + writeTextGeneratedSync(join(stage, "payload"), parts, (slice) => hash.update(slice)); writeFileSync(join(stage, "digest"), hash.digest("hex") + "\n"); const path = join(directory, key); renameSync(join(stage, "payload"), path); diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index d4487aba4..2c850cb14 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -56,7 +56,7 @@ import { clearFenceEvalCaches } from "../library/fence-eval.js"; import { archiveNativeLibrary, localizeNativeLibrary, linkNativeWasmLibrary } from "./library.js"; import type { NativeToolchain } from "./toolchain.js"; import { evaluateNativeComptime } from "./comptime.js"; -import { contentDigest, type NativeCache } from "./cache.js"; +import { contentDigest, fileDigest, type NativeCache } from "./cache.js"; import { nativeProfileKey, nativeProfileRefusal, @@ -186,7 +186,7 @@ export class NativeCompiler { key = contentDigest( JSON.stringify({ schema: 1, - llvm: contentDigest(readFileSync(input)), + llvm: fileDigest(input), outputKind, optimization, source, @@ -361,7 +361,8 @@ export class NativeCompiler { const inputDirectory = join(stage, "input"); mkdirSync(inputDirectory); const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll"); - writeFileSync(llvmInput, llvm); + // In slices: the module text is never encoded whole. + writeTextPartsSync(llvmInput, [llvm]); timing("llvm-write"); if (outputKind === "llvm") { renameSync(stagedOutput, output); @@ -495,7 +496,7 @@ export class NativeCompiler { renameSync(stagedOutput + ".dSYM", output + ".dSYM"); } mkdirSync(options.outDir, { recursive: true }); - writeFileSync(llvmPath, llvm); + writeTextPartsSync(llvmPath, [llvm]); let irPath: string | undefined; if (options.emitIr) { irPath = join(options.outDir, `${stem}.ir.json`); @@ -583,7 +584,8 @@ export class NativeCompiler { stage = mkdtempSync(join(dirname(archivePath), ".scriptc-library-")); const llvm = prepared.llvm; const llvmInput = join(stage, "program.ll"); - writeFileSync(llvmInput, llvm); + // In slices: the module text is never encoded whole. + writeTextPartsSync(llvmInput, [llvm]); timing("llvm-write"); const features = prepared.features; const pack = selectNativeRuntimePack( @@ -646,7 +648,7 @@ export class NativeCompiler { }); } timing("native-archive"); - writeFileSync(llvmPath, llvm); + writeTextPartsSync(llvmPath, [llvm]); let irPath: string | undefined; if (options.emitIr) { irPath = join(options.outDir, `${stem}.lib.ir.json`); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 6b21b2230..edc90c449 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -29,7 +29,7 @@ import type { LibraryProfile } from "../library/library-profile.js"; import type { IrModule } from "../ir/ir.js"; import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js"; import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js"; -import { batchTextParts } from "./text-file.js"; +import { batchTextParts, textSlices } from "./text-file.js"; import type { NativeToolchain } from "./toolchain.js"; import { nativeFileIdentity } from "./file-identity.js"; @@ -58,37 +58,57 @@ interface FrontendCacheEntry { ir: string[] | null; } -function frontendCacheParts(probes: FrontendInputSnapshot, input: NativeExecutableInput): string[] { +/** The entry's JSON text, generated as it is written. The LLVM text is + * escaped in slices (`textSlices`), the same JSON string as escaping it + * whole, so a large module is never copied whole. */ +function* frontendCacheParts( + probes: FrontendInputSnapshot, + input: NativeExecutableInput, +): Generator { const metadata = JSON.stringify({ schema: "scriptc.native-frontend.v3", probes, input: { ...input, llvm: "", ir: null }, }); - const parts = [metadata.slice(0, metadata.length - 1) + ',"llvm":', JSON.stringify(input.llvm)]; - if (input.ir === null) parts.push(',"ir":null}'); + yield metadata.slice(0, metadata.length - 1) + ',"llvm":"'; + for (const slice of textSlices(input.llvm)) yield JSON.stringify(slice).slice(1, -1); + yield '"'; + if (input.ir === null) yield ',"ir":null}'; else { - parts.push(',"ir":['); + yield ',"ir":['; for (let index = 0; index < input.ir.length; index++) { - if (index > 0) parts.push(","); - parts.push(JSON.stringify(input.ir[index]!)); + if (index > 0) yield ","; + yield JSON.stringify(input.ir[index]!); } - parts.push("]}"); + yield "]}"; + } +} + +/** The parts while their total length stays within `limit`; past it, a + * throw abandons the write. */ +function* boundedParts(parts: Generator, limit: number): Generator { + let length = 0; + for (const part of parts) { + length += part.length; + if (length > limit) throw new Error("frontend cache entry exceeds one native string"); + yield part; } - return parts; } /** Publish an entry only when a native reader can decode it as one string; - * UTF-16 length never exceeds the UTF-8 byte length. */ + * UTF-16 length never exceeds the UTF-8 byte length. A longer entry + * abandons its write, which writeParts discards. */ function publishFrontendCache( cache: NativeCache, key: string, probes: FrontendInputSnapshot, input: NativeExecutableInput, ): void { - const parts = frontendCacheParts(probes, input); - let length = 0; - for (const part of parts) length += part.length; - if (length <= NATIVE_STRING_BYTES) cache.writeParts("frontend", key, parts); + cache.writeParts( + "frontend", + key, + boundedParts(frontendCacheParts(probes, input), NATIVE_STRING_BYTES), + ); } function readFrontendCache( diff --git a/packages/compiler/src/native/text-file.test.ts b/packages/compiler/src/native/text-file.test.ts new file mode 100644 index 000000000..b497bc4a2 --- /dev/null +++ b/packages/compiler/src/native/text-file.test.ts @@ -0,0 +1,42 @@ +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { textSlices, writeTextPartsSync } from "./text-file.js"; + +const directories: string[] = []; +afterEach(() => { + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +test("text slices keep surrogate pairs whole and escape as the whole text does", () => { + const text = 'ab😀cé😀😀\ud800x"\\\n' + "z".repeat(5); + for (let length = 2; length <= 8; length++) { + const slices = [...textSlices(text, length)]; + expect(slices.join("")).toBe(text); + for (const slice of slices) { + expect(slice.length).toBeLessThanOrEqual(length); + const last = slice.charCodeAt(slice.length - 1); + // A lone high surrogate of the text may end a slice; a pair's never does. + if (last >= 0xd800 && last <= 0xdbff) expect(slice.endsWith("\ud800")).toBe(true); + } + expect(slices.map((slice) => JSON.stringify(slice).slice(1, -1)).join("")).toBe( + JSON.stringify(text).slice(1, -1), + ); + } + expect([...textSlices("")]).toEqual([]); +}); + +test("a part larger than one slice is written as its UTF-8 bytes", () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-text-file-")); + directories.push(dir); + // A surrogate pair across the first slice boundary (1 MiB units). + const large = "a".repeat(1024 * 1024 - 1) + "😀" + "b".repeat(1024 * 1024 + 3); + const path = join(dir, "out.txt"); + const seen: string[] = []; + writeTextPartsSync(path, ["head,", large, "", "tail"], (slice) => seen.push(slice)); + const expected = Buffer.from("head," + large + "tail", "utf8"); + expect(readFileSync(path).equals(expected)).toBe(true); + expect(seen.join("")).toBe("head," + large + "tail"); + expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024); +}); diff --git a/packages/compiler/src/native/text-file.ts b/packages/compiler/src/native/text-file.ts index 6f8990339..d377c5273 100644 --- a/packages/compiler/src/native/text-file.ts +++ b/packages/compiler/src/native/text-file.ts @@ -24,17 +24,61 @@ export function batchTextParts(parts: readonly string[]): string[] { return batches; } -/** Write text parts to `path` in order without joining them first. Each part - * is one write; pass batched parts (`batchTextParts`) for many small ones. */ -export function writeTextPartsSync(path: string, parts: readonly string[]): void { +/** The most UTF-16 units of a part that one write encodes. */ +const TEXT_WRITE_LENGTH = 1024 * 1024; + +/** `text` in slices of at most `length` (2 or more) UTF-16 units. A slice + * never ends between the two halves of a surrogate pair, so the slices + * encode, as UTF-8 or as JSON string contents, to the encoding of `text`. */ +export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator { + for (let start = 0; start < text.length;) { + let end = Math.min(text.length, start + length); + if (end < text.length) { + const unit = text.charCodeAt(end - 1); + if (unit >= 0xd800 && unit <= 0xdbff) end--; + } + yield text.slice(start, end); + start = end; + } +} + +/** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large + * part is never encoded whole. `written` sees each slice after it is + * written. */ +function writeTextPart(fd: number, part: string, written?: (slice: string) => void): void { + for (const slice of textSlices(part)) { + const bytes = Buffer.from(slice, "utf8"); + for (let offset = 0; offset < bytes.length;) { + offset += writeSync(fd, bytes, offset, bytes.length - offset); + } + written?.(slice); + } +} + +/** Write text parts to `path` in order without joining them first; pass + * batched parts (`batchTextParts`) for many small ones. */ +export function writeTextPartsSync( + path: string, + parts: readonly string[], + written?: (slice: string) => void, +): void { const fd = openSync(path, "w"); try { - for (const part of parts) { - const bytes = Buffer.from(part, "utf8"); - for (let offset = 0; offset < bytes.length;) { - offset += writeSync(fd, bytes, offset, bytes.length - offset); - } - } + for (const part of parts) writeTextPart(fd, part, written); + } finally { + closeSync(fd); + } +} + +/** writeTextPartsSync for parts generated while they are written. */ +export function writeTextGeneratedSync( + path: string, + parts: Generator, + written?: (slice: string) => void, +): void { + const fd = openSync(path, "w"); + try { + for (const part of parts) writeTextPart(fd, part, written); } finally { closeSync(fd); } From de28aeca20a53fbeb0d2b5b17153e749d8d19c2d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:24:10 -0500 Subject: [PATCH 056/169] Map JavaScript [] literal fields dynamically and keep union-added fields unit-only Two JavaScript object literal mappings from #1799 broke packages. A property initialized to `[]` has the checker type `never[]`, and never's element maps to f64. A literal with such a property builds as a checked-dynamic object when it lowers on its own. A literal nested in one that maps to a record (`{ common: { issues: [] }, path: params?.path || [] }`, zod 3's parse context) lowers against its record type, so the property was a `number[]`. `ctx.common.issues.push(issue)` then threw "TypeError: expected number at $, got object", where Node pushes the record. Such a property now maps to the checked-dynamic array, as a property initialized to `undefined` or `null` maps to the checked-dynamic value. That `undefined`/`null` rule also matched the properties the checker adds when it normalizes a union of literals: `{ _tag, value } | { _tag, error }` gains an optional `error?: undefined` and `value?: undefined`. Mapping those to the checked-dynamic value let `{ _tag, value }` fit both arms, and Effect 4's CLI parser (`consumeFlagValue`) failed with SC2003 "union conversion has no unambiguous layout mapping". The rule now skips optional properties, so they keep their unit-only type and each literal converts to its own arm. --- packages/compiler/src/frontend/type-mapper.ts | 20 ++++++++++++ .../main.ts.json | 6 ++++ .../main.ts | 10 ++++++ .../node_modules/mini/index.d.ts | 5 +++ .../node_modules/mini/index.js | 31 +++++++++++++++++++ .../node_modules/mini/package.json | 1 + .../main.ts.json | 1 + 7 files changed, 74 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json create mode 100644 tests/corpus/7014-js-never-array-literal-fields/main.ts create mode 100644 tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts create mode 100644 tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js create mode 100644 tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json create mode 100644 tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index d64237846..4e8812437 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -5893,6 +5893,18 @@ function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChe ); } +/** A JavaScript object literal's property typed `never[]` (`{ issues: [] }`). + * Nothing checks a JavaScript file's later writes, which can push any value + * onto it (`ctx.common.issues.push(issue)`), so it holds the checked-dynamic + * array rather than never's f64 element array. */ +function jsNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + return ( + jsObjectLiteralProperty(p) && + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + /** A JavaScript object literal's property (`{ lastComponent: undefined }`, * `{ user: null }`). Nothing checks a JavaScript file's later writes, which * can store any value in it (`path.lastComponent = { ... }`), so a @@ -5904,6 +5916,12 @@ function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean { return def !== undefined && def.arms.every((arm) => isUnitType(arm)); } +/** A property a JavaScript object literal declares. The checker also adds + * each missing property, optional and `undefined`, to every literal of a + * union of literals (`{ _tag, value } | { _tag, error }` gains + * `error?: undefined` and `value?: undefined`); those keep their unit-only + * type, so a literal still converts to the one arm whose properties it + * has. */ function jsObjectLiteralProperty(p: ts.Symbol): boolean { const d = p.valueDeclaration; // `__proto__: null` sets the literal's prototype; it declares no property. @@ -5911,6 +5929,7 @@ function jsObjectLiteralProperty(p: ts.Symbol): boolean { d !== undefined && (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) && ts.symbolName(p) !== "__proto__" && + (p.flags & ts.SymbolFlags.Optional) === 0 && isJsSourceFile(ts.getSourceFileOfNode(d)) ); } @@ -7983,6 +8002,7 @@ function mapRecordTypeInner( if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN; + if (pt?.kind === "array" && jsNeverArrayProp(p, fieldTs, checker)) pt = DYN; // dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples): // the slot carries a dyn value exactly like an `unknown`-valued // overflow entry — same RC adapters, same dynFrom conversion on the diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..a67c77476 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7014-js-never-array-literal-fields/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7014-js-never-array-literal-fields/main.ts b/tests/corpus/7014-js-never-array-literal-fields/main.ts new file mode 100644 index 000000000..cf6d235c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package whose object literals start an array property as +// `[]` inside a literal that maps to a record, as zod 3's parse context does +// (`{ common: { issues: [] }, path: params?.path || [] }`), and later push +// records onto it. Node runs the pushes; each array holds them. +import { Schema, rows } from "mini"; + +const s = new Schema(); +console.log(JSON.stringify(s.safeParse(1)), JSON.stringify(s.safeParse("a"))); +console.log(JSON.stringify(s.safeParse(2, { path: ["root"] }))); +console.log(rows()); diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts new file mode 100644 index 000000000..5925684c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts @@ -0,0 +1,5 @@ +export interface Issue { code: string; path: (string | number)[]; message: string } +export declare class Schema { + safeParse(data: unknown, params?: { async?: boolean; path?: (string | number)[] }): { success: true; data: string } | { success: false; issues: Issue[] }; +} +export declare function rows(): string; diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js new file mode 100644 index 000000000..47d24c8ad --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js @@ -0,0 +1,31 @@ +// A parse context as zod 3 keeps it: the issue list starts as `[]` in a +// nested literal, and the path is the caller's array or `[]`. +export function makeIssue(params) { + const { path, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + return { ...issueData, path: fullPath, message: "m" }; +} +export function addIssue(ctx, issueData) { + const issue = makeIssue({ issueData, path: ctx.path }); + ctx.common.issues.push(issue); +} +export class Schema { + safeParse(data, params) { + const ctx = { + common: { issues: [], async: params?.async ?? false }, + path: params?.path || [], + data, + }; + if (typeof data !== "string") addIssue(ctx, { code: "invalid_type" }); + return ctx.common.issues.length === 0 ? { success: true, data } : { success: false, issues: ctx.common.issues }; + } +} +// Array properties of records in an array literal and in a deeper literal, +// filled after construction. +export function rows() { + const table = { rows: [{ cells: [] }, { cells: [] }], meta: { tags: { list: [] } }, size: 2 }; + table.rows[0].cells.push({ v: 1 }); + table.rows[1].cells.push("text", 3); + table.meta.tags.list.push({ name: "t" }, null); + return JSON.stringify(table); +} diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json new file mode 100644 index 000000000..be8bc49a2 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json @@ -0,0 +1 @@ +{ "name": "mini", "version": "1.0.0", "type": "module", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..c5bf12c38 --- /dev/null +++ b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3004, "san": 3359 } From e3e37940f1acfc16e2a9d80b215a0e08f2177c19 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:27:44 -0500 Subject: [PATCH 057/169] Iterate iterable unions without an array arm and adapt into optional Iterables for-of over an iterable union read its array arm without checking that it had one, so iterating a `Set | Generator` crashed the compiler; a union without an array arm now runs the generator loop only. A generator with a non-void return channel adapted into a required Iterable slot but not into an optional or nullable one, where the conversion failed with SC2003; the slot conversion now accepts iterable unions with undefined or null arms beside the iterable arms. --- .../src/frontend/lowering/iterable-values.ts | 35 +++++++++---- .../7383-iterable-union-sources.ts.json | 6 +++ tests/corpus/7383-iterable-union-sources.ts | 51 +++++++++++++++++++ .../7383-iterable-union-sources.ts.json | 1 + 4 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json create mode 100644 tests/corpus/7383-iterable-union-sources.ts create mode 100644 tests/harness/corpus-costs/7383-iterable-union-sources.ts.json diff --git a/packages/compiler/src/frontend/lowering/iterable-values.ts b/packages/compiler/src/frontend/lowering/iterable-values.ts index cd5ef53c6..78de9720c 100644 --- a/packages/compiler/src/frontend/lowering/iterable-values.ts +++ b/packages/compiler/src/frontend/lowering/iterable-values.ts @@ -47,14 +47,23 @@ export interface IterableUnion { /** The iterable union `t` names, or null. */ export function iterableUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { + const info = iterableSlotUnionOf(lowerer, t); + // An optional Iterable (`Iterable | undefined`) is consumed only once + // narrowed to the iterable union itself. + if (info === null || info.arms.some((arm) => arm.kind === "undefinedT" || arm.kind === "nullT")) + return null; + return info; +} + +/** The iterable union `t` names, also an optional or nullable one + * (`Iterable | undefined`, whose unit arms sit beside the iterable + * arms), or null. */ +function iterableSlotUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { if (t.kind !== "union") return null; const arms = lowerer.unions.get(t.unionId)?.arms; if (!arms) return null; const elem = iterableArmsElem(arms); - // An optional Iterable (`Iterable | undefined`) is consumed only once - // narrowed to the iterable union itself. - if (elem === null || arms.some((arm) => arm.kind === "undefinedT" || arm.kind === "nullT")) - return null; + if (elem === null) return null; const gen = arms.find((arm) => arm.kind === "generator") as GenType; return { unionId: t.unionId, type: t, elem, arms, gen }; } @@ -65,13 +74,14 @@ export function iterableGeneratorArm(lowerer: Lowerer, t: IrType): IrType { return iterableUnionOf(lowerer, t)?.gen ?? t; } -/** A value flowing into an iterable-typed slot: a source arm wraps as - * itself; a generator over other channel types or a checked-dynamic - * iterator (a Map or Set iterator) adapts into the generator arm. Null - * when neither applies, leaving the ordinary conversion fences to refuse - * other sources (class iterables, typed arrays, other element types). */ +/** A value flowing into an iterable-typed slot, optional or nullable + * ones included: a source arm wraps as itself; a generator over other + * channel types or a checked-dynamic iterator (a Map or Set iterator) + * adapts into the generator arm. Null when neither applies, leaving the + * ordinary conversion fences to refuse other sources (class iterables, + * typed arrays, other element types). */ export function iterableSlotValue(lowerer: Lowerer, expr: IrExpr, expected: IrType): IrExpr | null { - const info = iterableUnionOf(lowerer, expected); + const info = iterableSlotUnionOf(lowerer, expected); if (info === null || expr.type.kind === "union") return null; const wrap = (value: IrExpr): IrExpr => ({ kind: "unionWrap", @@ -232,8 +242,11 @@ export function lowerForOfIterable( iterableGenerator(lowerer, source, info) as IrExpr & { type: GenType }, labels, ); - if (stmt.awaitModifier || declaresClass(stmt.statement)) return generatorLoop(iterable); const arrayTag = info.arms.findIndex((arm) => arm.kind === "array"); + // A union without an array arm (`Set | Generator`) has only the + // generator loop. + if (stmt.awaitModifier || declaresClass(stmt.statement) || arrayTag < 0) + return generatorLoop(iterable); const arrT = info.arms[arrayTag]!; lowerer.scopes.push(new Map()); try { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..ea8476397 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-iterable-union-sources.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7383-iterable-union-sources.ts b/tests/corpus/7383-iterable-union-sources.ts new file mode 100644 index 000000000..3c393b256 --- /dev/null +++ b/tests/corpus/7383-iterable-union-sources.ts @@ -0,0 +1,51 @@ +// Iterable unions without an array arm, and optional or nullable Iterable +// slots that take a generator whose return channel is not void. + +function* evens(limit: number): Generator { + for (let i = 0; i <= limit; i += 2) yield i; +} + +// A union of a Set and a generator is an iterable union with no array arm: +// for-of walks every arm through the generator loop. +function total(src: Set | Generator): number { + let sum = 0; + for (const v of src) sum += v; + return sum; +} +console.log(total(new Set([1, 2, 3])), total(evens(6))); + +const labelled = (src: Set | Generator): string[] => { + const out: string[] = []; + outer: for (const v of src) { + if (v > 4) break outer; + out.push(`#${v}`); + } + return out; +}; +console.log(labelled(new Set([1, 3, 5])).join(" "), labelled(evens(10)).join(" ")); + +// A generator that returns a value: its return channel is dropped when it +// flows into an Iterable, required, optional or nullable alike. +function* countdown(from: number): Generator { + for (let i = from; i > 0; i--) yield i; + return "liftoff"; +} + +function joined(xs: Iterable): string { + return [...xs].join(","); +} + +function maybeJoined(xs?: Iterable): string { + return xs === undefined ? "none" : [...xs].join(","); +} + +function nullableJoined(xs: Iterable | null): string { + if (xs === null) return "null"; + let out = ""; + for (const x of xs) out += `${x};`; + return out; +} + +console.log(joined(countdown(3))); +console.log(maybeJoined(countdown(2)), maybeJoined(), maybeJoined(undefined), maybeJoined([7, 8])); +console.log(nullableJoined(countdown(4)), nullableJoined(null), nullableJoined(new Set([9]))); diff --git a/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } From cbafabe6102694f99cca5316b0f486525c85381c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:48:16 -0700 Subject: [PATCH 058/169] Keep the global object behind a typeof globalThis intersection cast `globalThis as typeof globalThis & { key?: T }` (the usual global singleton spelling) matched the WebAssembly cast lowering, because the target inherits the global's own WebAssembly member, and produced a fresh object holding only WebAssembly on every evaluation. Writes through the view were lost, so a `g[key] ??= new Repo()` singleton was rebuilt on every call and its state never persisted. Take the WebAssembly view only when the target's member is not the global's own, and map an intersection that contains the intrinsic `typeof globalThis` to the global object (dyn), as `typeof globalThis` alone already maps. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/builtins/webassembly.ts | 12 +++-- packages/compiler/src/frontend/type-mapper.ts | 12 +++-- ...-globalthis-intersection-singleton.ts.json | 6 +++ .../7362-globalthis-intersection-singleton.ts | 46 +++++++++++++++++++ ...-globalthis-intersection-singleton.ts.json | 1 + 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json create mode 100644 tests/corpus/7362-globalthis-intersection-singleton.ts create mode 100644 tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts index 56347e437..8e5c3a048 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts @@ -96,9 +96,15 @@ function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | nul isGlobalThis(lowerer, expr.expression) ) { const target = lowerer.checker.getTypeAtLocation(expr.type); - return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined - ? "globalThis.WebAssembly" - : null; + const member = lowerer.checker.getPropertyOfType(target, "WebAssembly"); + // A target built from `typeof globalThis` (`typeof globalThis & { key?: T }`) + // inherits the global's own WebAssembly declaration: that cast names the + // whole global object, whose other properties stay readable and writable. + const globalMember = lowerer.checker.getPropertyOfType( + lowerer.typeOf(bare(expr.expression)), + "WebAssembly", + ); + return member !== undefined && member !== globalMember ? "globalThis.WebAssembly" : null; } const node = bare(expr); if (ts.isIdentifier(node)) { diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 14379f30f..867b24f00 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1844,10 +1844,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // TypeScript's intrinsic globalThis symbol has no declaration. A stored // reference needs native object identity, including var's undefined state. const intrinsic = type.getSymbol(); + const isIntrinsicGlobal = (symbol: ts.Symbol | undefined): boolean => + symbol !== undefined && + ts.symbolName(symbol) === "globalThis" && + symbol.declarations.length === 0; + if (isIntrinsicGlobal(intrinsic)) return DYN; + // `typeof globalThis & { key?: T }` (the global-singleton spelling) is the + // same global object with declared extra members. if ( - intrinsic !== undefined && - ts.symbolName(intrinsic) === "globalThis" && - intrinsic.declarations.length === 0 + type.isIntersectionType() && + ts.constituentTypes(type).some((part) => isIntrinsicGlobal(part.getSymbol())) ) return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json new file mode 100644 index 000000000..74a21dfc5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-globalthis-intersection-singleton.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7362-globalthis-intersection-singleton.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7362-globalthis-intersection-singleton.ts b/tests/corpus/7362-globalthis-intersection-singleton.ts new file mode 100644 index 000000000..4c6eb997d --- /dev/null +++ b/tests/corpus/7362-globalthis-intersection-singleton.ts @@ -0,0 +1,46 @@ +// The global-singleton spelling: `globalThis as typeof globalThis & { key?: T }` +// names the global object itself (it also carries the global's own +// WebAssembly member), so a value stored through one view survives the +// call and is visible through every other view. +class Repository { + private items = new Map(); + add(name: string): number { + this.items.set(name, this.items.size + 1); + return this.items.size; + } + get size(): number { + return this.items.size; + } +} + +let constructed = 0; +const repoKey = "__exampleRepo"; +type WithRepo = typeof globalThis & { [repoKey]?: Repository }; + +function repo(): Repository { + const g = globalThis as WithRepo; + g[repoKey] ??= (constructed++, new Repository()); + return g[repoKey]; +} + +repo().add("keyboard"); +repo().add("mouse"); +console.log(constructed, repo().size, repo() === repo()); + +// a named member through a second spelling of the same intersection +const counters = globalThis as typeof globalThis & { exampleHits?: number; exampleLabel?: string }; +counters.exampleHits = 1; +counters.exampleHits += 2; +counters.exampleLabel = "ready"; +console.log( + (globalThis as typeof globalThis & { exampleHits?: number }).exampleHits, + (globalThis as unknown as { exampleLabel?: string }).exampleLabel, +); + +// the view is the global object +console.log(counters.globalThis === globalThis, (globalThis as WithRepo) === globalThis); + +// many reads of the global object in a loop see the same object +let same = 0; +for (let i = 0; i < 1000; i++) if ((globalThis as WithRepo)[repoKey] === repo()) same++; +console.log(same, constructed); diff --git a/tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json b/tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7362-globalthis-intersection-singleton.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } From 481f9a5e3194af4d0b2ad8a42a97691caba73252 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 16:18:31 -0700 Subject: [PATCH 059/169] Stringify live typed records and arrays without a key list JSON.stringify of a value reached through `unknown` walks native records and arrays through their live capsules. The protocol walk built a key-string array for every record and formatted every array index with snprintf into a fresh string, only to look the member up again by that key. A JSON API response of a few records spent most of its serialization time there. Without a replacer, walk a live record by field index over a snapshot of its present fields (its keys are the immortal field names) and a live array by element index, forming an index string only for a toJSON call that receives it. Members are still read as scr_json_member reads them, so callbacks that mutate or materialize the container see the same results. Index keys elsewhere use the integer formatter. Co-Authored-By: Claude Opus 5.5 --- .../7363-json-stringify-live-capsules.ts.json | 6 + packages/runtime/src/scr_json.c | 106 +++++++++++++++++- .../7363-json-stringify-live-capsules.ts | 73 ++++++++++++ .../7363-json-stringify-live-capsules.ts.json | 1 + 4 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json create mode 100644 tests/corpus/7363-json-stringify-live-capsules.ts create mode 100644 tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json new file mode 100644 index 000000000..580e84ff0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-json-stringify-live-capsules.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7363-json-stringify-live-capsules.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index ee45f6d93..45c963562 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10733,8 +10733,9 @@ static bool scr_json_callback_depth(size_t depth) { static ScrStr *scr_json_index_key(size_t index) { char bytes[32]; - int length = snprintf(bytes, sizeof bytes, "%zu", index); - return scr_str_new(bytes, (size_t)length); + /* array indexes are below 2^32: exact doubles, printed as integers */ + size_t length = scr_f64_to_str((double)index, bytes); + return scr_str_new(bytes, length); } /* Keys passed here come from our own index formatter or own-key walk. */ @@ -11010,12 +11011,22 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) { * { type, data } view, and an object with a callable toJSON through that * method, called with the key. Returns the value to serialize (+1), or NULL * with an exception pending. */ -static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { +/* The toJSON key argument: `key`, or the decimal spelling of array index + * `index` when `key` is NULL (formed only for a call that receives it). */ +static ScrDyn *scr_json_key_arg(const ScrStr *key, size_t index) { + if (key) return scr_dyn_new_str((ScrStr *)key); + ScrStr *text = scr_json_index_key(index); + ScrDyn *name = scr_dyn_new_str(text); + scr_str_release(text); + return name; +} + +static ScrDyn *scr_json_to_json_at(ScrDyn *value, const ScrStr *key, size_t index) { /* An instance of a class declared `extends Date` converts through its * toJSON, declared or inherited. */ double time; if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(value, &time)) { - ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *name = scr_json_key_arg(key, index); ScrDyn *converted = scr_dyn_date_class_to_json(value, name); scr_dyn_release(name); scr_dyn_release(value); @@ -11043,7 +11054,7 @@ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { ScrDyn *method = scr_dyn_obj_read(inspected, "toJSON", 6); if (!method) { scr_dyn_release(view); scr_dyn_release(value); return NULL; } if (method->kind == SCR_DYN_FUNC) { - ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *name = scr_json_key_arg(key, index); ScrDyn *args[] = { name }; scr_dyn_this_push_dyn(value); ScrDyn *converted = scr_dyn_call(method, args, 1, "toJSON"); @@ -11058,6 +11069,10 @@ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { return value; } +static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { + return scr_json_to_json_at(value, key, 0); +} + static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const ScrDyn *replacer) { ScrDyn *value = scr_json_member(holder, key); if (!value) return NULL; @@ -11192,6 +11207,81 @@ static ScrDyn *scr_json_property_list(const ScrDyn *replacer) { return list; } +/* The protocol walk over a live record or array capsule without a replacer + * (the caller entered it on the circular-structure stack). Members are read + * from the native value exactly as scr_json_member reads them, but the walk + * needs no key list: a record's present fields are snapshotted by index (its + * keys are the immortal field names) and an array's indexes are counted, so + * an index key string is formed only for a toJSON call that receives it. */ +static bool scr_json_live_write(ScrJsonBuf *buffer, const ScrDyn *value, bool array, + const ScrStr *gap, size_t depth) { + const ScrDynTypedRecordOps *record = array ? NULL : scr_typed_ref_record_ops(value); + /* EnumerableOwnProperties runs once, before any member's toJSON (the + * caller sends records of more than 64 fields down the key-list walk). */ + uint64_t present = 0; + size_t length; + if (array) { + length = ((ScrArr *)value->v.typed_ref.ptr)->len; + } else { + length = record->count; + for (size_t i = 0; i < length; i++) + if (record->fixed || record->has(value->v.typed_ref.ptr, i)) present |= (uint64_t)1 << i; + } + scr_jb_putc(buffer, array ? '[' : '{'); + bool first = true; + for (size_t i = 0; i < length; i++) { + if (!array && !(present & ((uint64_t)1 << i))) continue; + const ScrStr *key = array ? NULL : record->names[i]; + ScrDyn *child; + if (array) { + scr_jb_edge_idx(buffer, i); + /* scr_json_member's index read, minus the key: a present element + * answers directly; holes, a length a callback shrank, a capsule a + * callback materialized and inherited event members take the keyed + * read */ + child = !scr_event_ref_hooks && scr_dyn_typed_ref_array_ops(value) ? scr_dyn_typed_ref_element(value, i) + : NULL; + if (!child) { + ScrStr *index = scr_json_index_key(i); + child = scr_json_member(value, index); + scr_str_release(index); + } + } else { + scr_jb_edge_key(buffer, key); + child = scr_json_member(value, key); + } + if (child) child = scr_json_to_json_at(child, key, i); + if (!child) return false; + if (!array && scr_json_omitted(child)) { + scr_dyn_release(child); + continue; + } + if (!first) scr_jb_putc(buffer, ','); + first = false; + scr_json_gap(buffer, gap, depth + 1); + if (!array) { + scr_jb_put_json_str(buffer, key); + scr_jb_putc(buffer, ':'); + if (gap->len) scr_jb_putc(buffer, ' '); + } + bool ok = true; + if (scr_json_omitted(child)) scr_jb_puts(buffer, "null"); + else if (scr_json_plain_value(child)) { + /* the protocol walk's depth check, then the primitive itself */ + ok = scr_json_callback_depth(depth + 1); + if (ok) { + scr_dyn_json_write(buffer, child); + ok = !scr_exc_pending(); + } + } else ok = scr_json_replaced_write(buffer, child, scr_dyn_undefined(), NULL, gap, depth + 1); + scr_dyn_release(child); + if (!ok) return false; + } + if (!first) scr_json_gap(buffer, gap, depth); + scr_jb_putc(buffer, array ? ']' : '}'); + return true; +} + /* `properties`, when present, is an array replacer's PropertyList: objects * serialize exactly those keys, own or inherited, in that order; arrays * keep all their indexes. */ @@ -11221,6 +11311,12 @@ static bool scr_json_replaced_write(ScrJsonBuf *buffer, const ScrDyn *value, * after the current frame leaves the traversal stack. */ const void *identity = view || live ? value->v.typed_ref.ptr : (const void *)value; if (!scr_jb_enter(buffer, identity, array)) { scr_dyn_release(view); return false; } + if (live && replacer->kind == SCR_DYN_UNDEF && !properties && + (array || scr_typed_ref_record_ops(value)->count <= 64)) { + bool ok = scr_json_live_write(buffer, value, array, gap, depth); + scr_jb_leave(buffer); + return ok; + } scr_jb_putc(buffer, array ? '[' : '{'); ScrDyn *keys = array ? NULL : properties ? scr_dyn_retain((ScrDyn *)properties) : scr_dyn_obj_keys(observed); if (scr_exc_pending()) { scr_dyn_release(keys); scr_dyn_release(view); scr_jb_leave(buffer); return false; } diff --git a/tests/corpus/7363-json-stringify-live-capsules.ts b/tests/corpus/7363-json-stringify-live-capsules.ts new file mode 100644 index 000000000..5522ede5f --- /dev/null +++ b/tests/corpus/7363-json-stringify-live-capsules.ts @@ -0,0 +1,73 @@ +// JSON.stringify of typed records and arrays reached through `unknown`: +// members are read from the native values in key order, toJSON receives the +// member key (an index string inside arrays), undefined members drop out of +// objects and become null in arrays, and the key list is fixed before any +// toJSON runs. +interface Product { + id: string; + name: string; + priceCents: number; + tags: string[]; + note?: string; +} + +function stringify(value: unknown, space?: number): string { + return JSON.stringify(value, undefined, space); +} + +const products: Product[] = [ + { id: "p1", name: "Keyboard", priceCents: 12900, tags: ["hardware", "input"] }, + { id: "p2", name: "Quote \"and\" slash \\", priceCents: 0.5, tags: [], note: "café ☕" }, +]; +console.log(stringify({ products, count: products.length })); +console.log(stringify(products, 2)); +const second: Product = products[1]!; +delete second.note; +console.log(stringify(second)); + +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + toJSON(key: string): string { + return `${key}:${(this.cents / 100).toFixed(2)}`; + } +} +const prices: Money[] = [new Money(100), new Money(2550)]; +console.log(stringify(prices), stringify({ total: new Money(5) })); + +const numbers: number[] = [1, -0, NaN, Infinity, 2.5, 1e21]; +console.log(stringify(numbers)); +const maybe: (string | undefined)[] = ["a", undefined, "c"]; +console.log(stringify(maybe), stringify({ maybe })); +const nested: number[][] = [[1, 2], [], [3]]; +console.log(stringify(nested, 1)); + +// a toJSON that deletes a later optional field: the field was present when +// the key list was taken, so its (now undefined) value is dropped +interface Order { + first: { toJSON(): string }; + note?: string; +} +const order: Order = { first: { toJSON: () => "x" }, note: "kept?" }; +order.first = { + toJSON: () => { + delete order.note; + return "first"; + }, +}; +console.log(stringify(order)); + +// circular structures through capsules report the path +interface Node { + name: string; + children: Node[]; +} +const root: Node = { name: "root", children: [] }; +root.children.push({ name: "leaf", children: [root] }); +try { + stringify(root); +} catch (error) { + console.log((error as Error).message); +} diff --git a/tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json b/tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7363-json-stringify-live-capsules.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } From 86a94cf07417455e73eafac66ae43e638666d9c9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:47:46 -0500 Subject: [PATCH 060/169] Copy unaligned AST word tables in one block --- .../src/frontend/ts7/ast-wire.test.ts | 24 +++++++++ .../compiler/src/frontend/ts7/ast-wire.ts | 49 +++++++++++-------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/frontend/ts7/ast-wire.test.ts b/packages/compiler/src/frontend/ts7/ast-wire.test.ts index 03a98a2dc..fd3e611e2 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.test.ts @@ -101,6 +101,30 @@ test("node words respect a byte view's offset, signed spans and unsigned flags", expect(() => file.kind(5)).toThrow(AstDecodeError); }); +test("strings, string offsets and node words read alike at every byte alignment", () => { + const original = response(); + for (let shift = 0; shift < 4; shift++) { + const padded = new Uint8Array(original.length + 8).fill(0xff); + padded.set(original, shift); + const bytes = padded.subarray(shift, shift + original.length); + const file = new AstWireFile(bytes); + expect([file.string(0), file.string(2), file.text(1), file.text(3)]).toEqual([ + "abcfile!", + "file", + "abcfile!", + "file", + ]); + expect([file.kind(3), file.next(3), file.parent(4), file.data(4)]).toEqual([ + AstKind.Identifier, + 4, + 2, + 0x40000002, + ]); + word(bytes, 56, 9); + expect(() => new AstWireFile(bytes)).toThrow("invalid string table offset"); + } +}); + test("truncated headers, sections and node tables are rejected", () => { for (let size = 0; size < HEADER_SIZE; size++) expect(() => new AstWireFile(new Uint8Array(size))).toThrow(AstDecodeError); diff --git a/packages/compiler/src/frontend/ts7/ast-wire.ts b/packages/compiler/src/frontend/ts7/ast-wire.ts index 9dd8c9f5c..6b2222eae 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -109,6 +109,20 @@ function invalidNodeIndex(index: number): never { * byte order. */ const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1; +/** `count` little-endian words of `bytes` from `offset`, which the caller has + * bounds-checked: a view of the response when the words are aligned on a + * little-endian host, a copy of their bytes when they are not, and a decoded + * copy on a big-endian host. Where a section starts depends on the lengths of + * the strings before it (file paths among them), so most files are unaligned. */ +function astWords(bytes: Uint8Array, offset: number, count: number): Uint32Array { + const start = bytes.byteOffset + offset; + if (LITTLE_ENDIAN && start % 4 === 0) return new Uint32Array(bytes.buffer, start, count); + const words = new Uint32Array(count); + if (LITTLE_ENDIAN) new Uint8Array(words.buffer).set(bytes.subarray(offset, offset + count * 4)); + else for (let i = 0; i < count; i++) words[i] = astU32(bytes, offset + i * 4); + return words; +} + function hexWord(value: number): string { const digits = "0123456789abcdef"; let result = ""; @@ -125,14 +139,15 @@ export class AstWireFile { readonly stringCount: number; readonly contentHash: string; readonly parseOptionsKey: string; - private readonly stringOffsets: number; private readonly strings: number; private readonly extended: number; private readonly structured: number; private readonly nodes: number; - /** The node table as 32-bit words: a view of the response when it is - * word-aligned on a little-endian host, otherwise a decoded copy. */ + /** The node table as 32-bit words (astWords). */ private readonly words: Uint32Array; + /** The string table's start and end offsets, two words per string + * (astWords). */ + private readonly stringWords: Uint32Array; /** Decoded strings by string word index / 2. */ private readonly stringCache: (string | undefined)[]; private preorderTree: AstPreorder | null | undefined; @@ -142,34 +157,28 @@ export class AstWireFile { const metadata = astU32(bytes, HEADER_OFFSET_METADATA); if (metadata >>> 24 !== PROTOCOL_VERSION) throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`); - this.stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS); + const stringOffsets = astU32(bytes, HEADER_OFFSET_STRING_TABLE_OFFSETS); this.strings = astU32(bytes, HEADER_OFFSET_STRING_TABLE); this.extended = astU32(bytes, HEADER_OFFSET_EXTENDED_DATA); this.structured = astU32(bytes, HEADER_OFFSET_STRUCTURED_DATA); this.nodes = astU32(bytes, HEADER_OFFSET_NODES); if ( - this.stringOffsets < HEADER_SIZE || - this.strings < this.stringOffsets || + stringOffsets < HEADER_SIZE || + this.strings < stringOffsets || this.extended < this.strings || this.structured < this.extended || this.nodes < this.structured || this.nodes > bytes.length || - (this.strings - this.stringOffsets) % 8 !== 0 || + (this.strings - stringOffsets) % 8 !== 0 || (this.structured - this.extended) % 4 !== 0 || (bytes.length - this.nodes) % NODE_LEN !== 0 ) { throw new AstDecodeError("invalid section layout"); } this.nodeCount = (bytes.length - this.nodes) / NODE_LEN; - this.stringCount = (this.strings - this.stringOffsets) / 8; - const wordCount = this.nodeCount * NODE_WORDS; - const start = bytes.byteOffset + this.nodes; - if (LITTLE_ENDIAN && start % 4 === 0) { - this.words = new Uint32Array(bytes.buffer, start, wordCount); - } else { - this.words = new Uint32Array(wordCount); - for (let i = 0; i < wordCount; i++) this.words[i] = astU32(bytes, this.nodes + i * 4); - } + this.stringCount = (this.strings - stringOffsets) / 8; + this.words = astWords(bytes, this.nodes, this.nodeCount * NODE_WORDS); + this.stringWords = astWords(bytes, stringOffsets, this.stringCount * 2); this.stringCache = new Array(this.stringCount).fill(undefined); if (this.nodeCount < 2) throw new AstDecodeError("missing root"); if (this.kind(1) === KIND_NODE_LIST || this.parent(1) > 1) @@ -184,8 +193,8 @@ export class AstWireFile { // source text, so pairs overlap and are not globally monotone. Handles // address words (0, 2, 4...), not pair ordinals. for (let i = 0; i < this.stringCount; i++) { - const start = astU32(bytes, this.stringOffsets + i * 8); - const end = astU32(bytes, this.stringOffsets + i * 8 + 4); + const start = this.stringWords[i * 2]!; + const end = this.stringWords[i * 2 + 1]!; if (end < start || end > this.extended - this.strings) throw new AstDecodeError("invalid string table offset"); } @@ -242,8 +251,8 @@ export class AstWireFile { throw new AstDecodeError(`invalid string index ${index}`); const cached = this.stringCache[index >>> 1]; if (cached !== undefined) return cached; - const start = astU32(this.bytes, this.stringOffsets + index * 4); - const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4); + const start = this.stringWords[index]!; + const end = this.stringWords[index + 1]!; const text = decodeAstString(this.bytes, this.strings + start, end - start); this.stringCache[index >>> 1] = text; return text; From bc6a824b5527cb73e5aa0e3b2e4146b444db7d2f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:59:44 -0500 Subject: [PATCH 061/169] Inline string releases in release builds Release builds called scr_str_release for every string release, so a record update by object spread paid a call for each string field it dropped. A string release now uses the emitted fast-path helper, as string retains do; in release builds a function with more than 8 string release sites keeps the runtime call at each, so string-heavy builders and module initializers grow little. Speed builds are unchanged. --- .../compiler/src/backend/llvm/emit-module.ts | 4 +- .../compiler/src/backend/llvm/emitter.test.ts | 18 +++++-- .../compiler/src/backend/llvm/shapes.test.ts | 31 +++++++++--- packages/compiler/src/backend/llvm/shapes.ts | 50 +++++++++++++------ 4 files changed, 75 insertions(+), 28 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 156ad8b8d..b6e489c19 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -28,7 +28,7 @@ import { } from "../../ir/ir.js"; import { FN_ATTRS, - limitInlineDynReleases, + limitInlineReleases, releaseSym, typedRefConstructor, vAdapters, @@ -2469,7 +2469,7 @@ export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; de export function emitParts(emitter: LlEmitter): string[] { const parts = [ - ...limitInlineDynReleases(emitter.emitModuleParts()), + ...limitInlineReleases(emitter.emitModuleParts(), emitter.shapeHost.rcMode), ...emitter.disposeObserverGates(), ]; if (!emitter.executableTls) return parts; diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index f413d6153..d1fd62a83 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -13,6 +13,7 @@ import { type IrType, } from "../../ir/ir.js"; import { emitLlvmModule, emitLlvmModuleSource, LlEmitter } from "./emitter.js"; +import { INLINE_STR_RELEASES_PER_FUNCTION } from "./shapes.js"; /** A pending-exception check: the inline active-cell test or a runtime call. */ const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/; @@ -201,17 +202,24 @@ test("detached exit defines the runtime opt-in symbol only when requested", () = expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain); }); -test("inline RC inlines every fast path at speed and keeps string and box releases as calls in release", () => { +test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => { for (const boxed of [false, true]) { const module = moduleFor(Array.from({ length: 4 }, call), { boxed }); const release = emitLlvmModule(module); expect(release).not.toContain("@sc_rc_"); expect(release).not.toContain("%ScrMapRc"); - // The release subset inlines string retains and dyn retains and releases - // only; these string and box releases stay runtime calls. + // The release subset inlines string and dyn retains and releases and Map + // retains only: box releases stay runtime calls, and so do the string + // releases of a function with more than INLINE_STR_RELEASES_PER_FUNCTION. const subset = emitLlvmModule(module, { inlineRc: "release" }); - expect(subset).not.toContain("@sc_rc_release_str"); - expect(subset).not.toContain("@sc_rc_release_box"); + expect(subset).not.toContain("call void @sc_rc_release_box("); + const sites = (ir: string, fn: string): number => ir.split(`call void @${fn}(ptr`).length - 1; + if (!boxed) { + expect(sites(subset, "sc_rc_release_str")).toBe(0); + // Every site calls the runtime, and so does the unused helper's slow path. + expect(sites(subset, "scr_str_release")).toBe(sites(release, "scr_str_release") + 1); + expect(sites(release, "scr_str_release")).toBeGreaterThan(INLINE_STR_RELEASES_PER_FUNCTION); + } const speed = emitLlvmModule(module, { inlineRc: "speed" }); const helper = boxed ? "sc_rc_release_box" : "sc_rc_release_str"; const runtime = boxed ? "scr_box_release" : "scr_str_release"; diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index 347099c16..f7327a688 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -6,8 +6,9 @@ import { emitRecordShapes, inlineRcDecls, INLINE_DYN_RELEASES_PER_FUNCTION, + INLINE_STR_RELEASES_PER_FUNCTION, inlineRcOption, - limitInlineDynReleases, + limitInlineReleases, releaseSym, retainSym, type ShapeHost, @@ -132,10 +133,10 @@ describe("LLVM runtime RC symbols", () => { ]); }); - test("release builds inline only string retains and dyn retains and releases", () => { + test("release builds inline only string and dyn retains and releases and Map retains", () => { const { host } = declarationHost(64, "release"); expect(retainSym(host, { kind: "string" })).toBe("@sc_rc_retain_str"); - expect(releaseSym(host, { kind: "string" })).toBe("@scr_str_release"); + expect(releaseSym(host, { kind: "string" })).toBe("@sc_rc_release_str"); expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn"); expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn"); expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe("@scr_union_release"); @@ -251,12 +252,12 @@ describe("LLVM runtime RC symbols", () => { }); }); -describe("inline dyn release budget", () => { - const body = (name: string, sites: number): string => +describe("inline release budgets", () => { + const body = (name: string, sites: number, family = "dyn"): string => [ `define internal void @${name}(ptr %v) #0 {`, "entry:", - ...Array.from({ length: sites }, () => " call void @sc_rc_release_dyn(ptr %v)"), + ...Array.from({ length: sites }, () => ` call void @sc_rc_release_${family}(ptr %v)`), " ret void", "}", "", @@ -266,7 +267,9 @@ describe("inline dyn release budget", () => { const small = body("small", INLINE_DYN_RELEASES_PER_FUNCTION); const large = body("large", INLINE_DYN_RELEASES_PER_FUNCTION + 1); // main's lines are separate parts. - const limited = limitInlineDynReleases([`; header\n${small}`, ...large.split("\n")]).join("\n"); + const limited = limitInlineReleases([`; header\n${small}`, ...large.split("\n")], "speed").join( + "\n", + ); expect(limited.startsWith("; header\n")).toBe(true); expect(limited).toContain(small); const out = limited.slice(limited.indexOf("@large(")); @@ -275,6 +278,20 @@ describe("inline dyn release budget", () => { INLINE_DYN_RELEASES_PER_FUNCTION + 1, ); }); + + test("a function over the string budget calls the runtime string release at every site", () => { + const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str"); + const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str"); + const limited = limitInlineReleases([small, large], "release").join("\n"); + expect(limited).toContain(small); + // Speed emission keeps every inline string release. + expect(limitInlineReleases([small, large], "speed").join("\n")).toBe([small, large].join("\n")); + const out = limited.slice(limited.indexOf("@large(")); + expect(out).not.toContain("@sc_rc_release_str("); + expect(out.split("call void @scr_str_release(ptr %v)").length - 1).toBe( + INLINE_STR_RELEASES_PER_FUNCTION + 1, + ); + }); }); describe("emitted object releases", () => { diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 382d2aa34..8ae24375e 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -578,15 +578,17 @@ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineR } /** Which RC fast paths an emission inlines. `speed` mirrors every family - * with a fast path. `release` mirrors only the string and Map retains and - * the dyn retain and release: the operations that dominate dynamic-value, - * string and Map code (a Map read from an array or another Map), at almost - * no size cost. String releases stay calls in `release` because - * string-heavy programs have many release sites. */ + * with a fast path. `release` mirrors only the string retain and release, + * the Map retain and the dyn retain and release: the operations that + * dominate dynamic-value, string and Map code (a Map read from an array or + * another Map). String-heavy programs have many string release sites, so a + * function with more than INLINE_STR_RELEASES_PER_FUNCTION of them calls the + * runtime release at each (limitInlineReleases). */ export type InlineRcMode = "release" | "speed"; const RELEASE_RC_OPS = new Set([ "str:retain", + "str:release", "map:retain", "dyn:retain", "dyn:release", @@ -670,12 +672,34 @@ function inlineRcName(family: string, op: RcOp): string { * the inline helper mirrors, at every site. */ export const INLINE_DYN_RELEASES_PER_FUNCTION = 256; -/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION} to each function of an - * emitted module, given as parts joined by newlines. A function can span - * parts (main's lines) and a part can hold several functions. */ -export function limitInlineDynReleases(parts: readonly string[]): string[] { - const call = `call void @${inlineRcName("dyn", "release")}(`; - const runtime = `call void @${INLINE_RC_FAMILIES["dyn"]!.release}(`; +/** Inlined string releases one function body keeps in `release` emission, + * which inlines string releases (RELEASE_RC_OPS); the bodies with the most of them, such as + * builders of string-heavy records and module initializers, would grow the + * most for the least gain, so a body with more calls scr_str_release at + * every site. */ +export const INLINE_STR_RELEASES_PER_FUNCTION = 8; + +/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION}, and in `release` emission + * {@link INLINE_STR_RELEASES_PER_FUNCTION}, to each function of an emitted + * module, given as parts joined by newlines. A function can span parts + * (main's lines) and a part can hold several functions. */ +export function limitInlineReleases( + parts: readonly string[], + mode: InlineRcMode | undefined, +): string[] { + const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION); + return mode === "release" + ? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION) + : dyn; +} + +function limitInlineReleaseFamily( + parts: readonly string[], + family: "dyn" | "str", + max: number, +): string[] { + const call = `call void @${inlineRcName(family, "release")}(`; + const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`; // The function each release site belongs to: the last `define` line // before it, counted over the joined text. const sites = ( @@ -701,9 +725,7 @@ export function limitInlineDynReleases(parts: readonly string[]): string[] { const counts = new Map(); let fn = 0; for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn); - const over = new Set( - [...counts].filter(([, n]) => n > INLINE_DYN_RELEASES_PER_FUNCTION).map(([f]) => f), - ); + const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f)); if (over.size === 0) return [...parts]; fn = 0; return parts.map((part) => { From 3024e57eeb833c717f936d493c212928d3eb6a40 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:02:41 -0500 Subject: [PATCH 062/169] Revert "Enumerate each record in the key order its object literal wrote (#1802)" --- docs/content/docs/coverage.mdx | 2 +- .../record-identity-and-property-order.mdx | 7 +- packages/compiler/src/backend/llvm/dyn.ts | 149 +++-- .../backend/llvm/expr-stream-bridges.test.ts | 2 +- .../src/backend/llvm/expr-stream-bridges.ts | 129 ++--- .../compiler/src/backend/llvm/key-orders.ts | 49 -- packages/compiler/src/backend/llvm/walkers.ts | 137 +++-- .../src/frontend/lowering/divergences.ts | 5 - .../lowering/key-order-witness.test.ts | 229 +------- .../frontend/lowering/key-order-witness.ts | 543 +----------------- .../src/frontend/lowering/lower-calls.ts | 24 +- .../src/frontend/lowering/lower-inspect.ts | 12 +- packages/compiler/src/frontend/type-mapper.ts | 17 +- .../src/ir/function-references.test.ts | 1 - packages/compiler/src/ir/ir.ts | 18 - .../tests/corpus/7132-key-order-json.ts.json | 6 - ...7133-key-order-keys-values-entries.ts.json | 6 - .../corpus/7134-key-order-for-in.ts.json | 6 - .../corpus/7135-key-order-inspect.ts.json | 6 - .../corpus/7136-key-order-copies.ts.json | 6 - .../corpus/7137-key-order-dynamic.ts.json | 6 - .../corpus/7138-key-order-late-writes.ts.json | 6 - packages/runtime/src/scr_dyn.h | 6 - packages/runtime/src/scr_json.c | 3 +- tests/corpus/7132-key-order-json.ts | 31 - .../7133-key-order-keys-values-entries.ts | 19 - tests/corpus/7134-key-order-for-in.ts | 15 - tests/corpus/7135-key-order-inspect.ts | 12 - tests/corpus/7136-key-order-copies.ts | 32 -- tests/corpus/7137-key-order-dynamic.ts | 30 - tests/corpus/7138-key-order-late-writes.ts | 44 -- tests/coverage-fixtures/divergences.ts | 10 +- .../__snapshots__/coverage-divergences.txt | 10 +- .../corpus-costs/7132-key-order-json.ts.json | 1 - ...7133-key-order-keys-values-entries.ts.json | 1 - .../7134-key-order-for-in.ts.json | 1 - .../7135-key-order-inspect.ts.json | 1 - .../7136-key-order-copies.ts.json | 1 - .../7137-key-order-dynamic.ts.json | 1 - .../7138-key-order-late-writes.ts.json | 1 - 40 files changed, 209 insertions(+), 1376 deletions(-) delete mode 100644 packages/compiler/src/backend/llvm/key-orders.ts delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json delete mode 100644 tests/corpus/7132-key-order-json.ts delete mode 100644 tests/corpus/7133-key-order-keys-values-entries.ts delete mode 100644 tests/corpus/7134-key-order-for-in.ts delete mode 100644 tests/corpus/7135-key-order-inspect.ts delete mode 100644 tests/corpus/7136-key-order-copies.ts delete mode 100644 tests/corpus/7137-key-order-dynamic.ts delete mode 100644 tests/corpus/7138-key-order-late-writes.ts delete mode 100644 tests/harness/corpus-costs/7132-key-order-json.ts.json delete mode 100644 tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json delete mode 100644 tests/harness/corpus-costs/7134-key-order-for-in.ts.json delete mode 100644 tests/harness/corpus-costs/7135-key-order-inspect.ts.json delete mode 100644 tests/harness/corpus-costs/7136-key-order-copies.ts.json delete mode 100644 tests/harness/corpus-costs/7137-key-order-dynamic.ts.json delete mode 100644 tests/harness/corpus-costs/7138-key-order-late-writes.ts.json diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index db1193b98..954b040db 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -97,7 +97,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them. - `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity. -- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. +- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index cc16f74d5..0381803f6 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -10,10 +10,9 @@ A type whose values are only read field by field, from fields the class does not Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference. -`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js. -scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. -When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last. -A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. +The order matches Node.js when every value of the type is created in the same order. +When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it. A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 04e3b9817..bb00e4563 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,4 +1,3 @@ -import { emitPerKeyOrder } from "./key-orders.js"; import { typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js"; @@ -38,8 +37,6 @@ import { accessorSlotProp, DYN_HANDLE_KINDS, isDirentShape, - KEY_ORDER_FIELD, - shapeKeyOrders, isDynTypedRefType, isRefCounted, isUnitType, @@ -1110,10 +1107,9 @@ export class LlDyn { } B.condBr(readable, l0, fail); B.startBlock(l0); - // dyn ('unknown') fields match ANY value, present or missing. The - // order index is no key. + // dyn ('unknown') fields match ANY value, present or missing. for (const f of shape.fields) { - if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue; + if (f.type.kind === "dyn") continue; const { value: m, release } = this.memberRead(B, f.name, f.type); const lNext = B.newLabel("dm.n"); this.pendingBail( @@ -1296,10 +1292,9 @@ export class LlDyn { return ( shape.fields.every( (f) => - f.name === KEY_ORDER_FIELD || - (!f.name.startsWith("%") && - f.name !== "__proto__" && - this.jsonSchemaSupported(f.type, visiting, true)), + !f.name.startsWith("%") && + f.name !== "__proto__" && + this.jsonSchemaSupported(f.type, visiting, true), ) && (!index || this.jsonSchemaSupported(index, visiting)) ); @@ -1385,9 +1380,7 @@ export class LlDyn { const base = host.nullableUnions.fieldBase(t.shapeId); const index = new Map(shape.fields.map((f, i) => [f.name, i + base])); // The runtime names kept-source keys by this index (record-sources.ts). - // The order index is no key: parsed values keep the table's order - // and their source's own order. - const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD); + const order = recordSourceFieldOrder(shape); if (recordMayKeepSource(shape)) this.keepsRecordSources = true; const fieldTy = `{ ptr, ${S}, ${S}, ptr }`; const entries = order.map((fieldName) => { @@ -2163,10 +2156,7 @@ export class LlDyn { } B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`); if (recursive) this.recordBuilt(B, name, "%r0"); - // The order index stays 0: the source's own order is kept below - // when it differs from the table. for (const f of shape.fields) { - if (f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); this.pendingBail( B, @@ -2899,79 +2889,70 @@ export class LlDyn { host.declare(`declare ptr @scr_dyn_new_obj()`); B.line(`${d} = call ptr @scr_dyn_new_obj()`); } - // Keys insert in DECLARED order (JS insertion order), or in the - // value's own order for a shape with orderVariants; internal - // '%'-fields follow so a record→dyn→record round trip keeps them - // (all but the order index, which is no key). + // Keys insert in DECLARED order (JS insertion order); internal + // '%'-fields follow so a record→dyn→record round trip keeps them. const byName = new Map(shape.fields.map((f) => [f.name, f])); // An accessor property is one key whose getter and setter are the // slot closures (see accessorSlotProp), in accessorOrder's place. - const orders = - shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape); - const perOrder = (order: readonly string[]): void => { - const inOrder = new Set(order); - const accessors = new Set( - shape.accessorOrder === undefined - ? [] - : shape.fields.flatMap((f) => { - const slot = accessorSlotProp(f.name); - return slot === null ? [] : [slot.prop]; - }), - ); - type DynField = { name: string; type: IrType } | { accessor: string }; - const dynFields: DynField[] = [ - ...order.flatMap((n) => { - if (accessors.has(n)) return [{ accessor: n } as DynField]; - const f = byName.get(n); - return f === undefined ? [] : [f as DynField]; - }), - ...shape.fields.filter( - (f) => - !inOrder.has(f.name) && - f.name !== KEY_ORDER_FIELD && - !(accessors.size > 0 && accessorSlotProp(f.name) !== null), - ), - ]; - for (const entry of dynFields) { - if ("accessor" in entry) { - host.declare( - `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, - ); - const boxed = (kind: "get" | "set"): string => { - const slot = byName.get(`%${kind}:${entry.accessor}`); - if (slot === undefined) return "null"; - const fv = loadFieldOf(slot.name, slot.type); - const conv = B.tmp(); - B.line( - `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, - ); - return conv; - }; - const getter = boxed("get"); - const setter = boxed("set"); - B.line( - `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, - ); - continue; - } - const f = entry; - const klen = Buffer.byteLength(f.name, "utf8"); - const fv = loadFieldOf(f.name, f.type); - // An ABSENT optional field contributes no key. - const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; - const conv = B.tmp(); - B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); - B.line( - `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name); + const inOrder = new Set(order); + const accessors = new Set( + shape.accessorOrder === undefined + ? [] + : shape.fields.flatMap((f) => { + const slot = accessorSlotProp(f.name); + return slot === null ? [] : [slot.prop]; + }), + ); + type DynField = { name: string; type: IrType } | { accessor: string }; + const dynFields: DynField[] = [ + ...order.flatMap((n) => { + if (accessors.has(n)) return [{ accessor: n } as DynField]; + const f = byName.get(n); + return f === undefined ? [] : [f as DynField]; + }), + ...shape.fields.filter( + (f) => + !inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null), + ), + ]; + for (const entry of dynFields) { + if ("accessor" in entry) { + host.declare( + `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, ); - if (lSkip !== null) { - B.br(lSkip); - B.startBlock(lSkip); - } + const boxed = (kind: "get" | "set"): string => { + const slot = byName.get(`%${kind}:${entry.accessor}`); + if (slot === undefined) return "null"; + const fv = loadFieldOf(slot.name, slot.type); + const conv = B.tmp(); + B.line( + `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + ); + return conv; + }; + const getter = boxed("get"); + const setter = boxed("set"); + B.line( + `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, + ); + continue; } - }; - if (orders.length === 1) perOrder(orders[0]!); - else emitPerKeyOrder(B, shape, base, "%v", perOrder); + const f = entry; + const klen = Buffer.byteLength(f.name, "utf8"); + const fv = loadFieldOf(f.name, f.type); + // An ABSENT optional field contributes no key. + const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; + const conv = B.tmp(); + B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); + B.line( + `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + ); + if (lSkip !== null) { + B.br(lSkip); + B.startBlock(lSkip); + } + } // A Dirent row takes Node's Dirent prototype (the type probes) and // its Symbol(type) key; %dtype stays readable but non-enumerable. if (isDirentShape(shape)) { diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts index 244b3afea..5d8f5f26f 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts @@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => { const llvm = emitLlvmModule(module, { pointerBits: 64 }); const tables = [ ...llvm.matchAll( - /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm, + /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{ i64 (\d+), /gm, ), ]; // root and leaf get field hooks; the record with an index-like field name diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index ceaca3778..7cb0fa772 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -13,8 +13,6 @@ import { classFieldAttributesName, isClassOwnEnumerableFieldName, isDirentShape, - KEY_ORDER_FIELD, - shapeKeyOrders, isDynTypedRefType, isRefCounted, shapeHasAccessorSlots, @@ -46,7 +44,6 @@ import type { NullableRefField } from "./nullable-fields.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; import { recordMayKeepSource } from "./record-sources.js"; -import { emitPerKeyOrder } from "./key-orders.js"; import { registerReplay } from "./emit-journal.js"; /** An owned (+1) union value for a nullable-pointer class field slot `p` @@ -1068,68 +1065,52 @@ export function streamTypedRefMaterializeAdapter( const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_obj()`); const byName = new Map(shape.fields.map((field) => [field.name, field])); - // A shape with orderVariants has one field list per key order; its - // order index is no key. - const orders = shapeKeyOrders(shape); - const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => { - const inOrder = new Set(order); - return [ - ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), - ...shape.fields.filter( - (field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD, - ), - ]; - }; + const order = shape.declaredOrder ?? shape.fields.map((field) => field.name); + const inOrder = new Set(order); + const fields = [ + ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), + ...shape.fields.filter((field) => !inOrder.has(field.name)), + ]; // Published before the field converters, like the array hooks below. // A Dirent row reads through its finished view (the prototype and // the non-enumerable %dtype). const dirent = isDirentShape(shape); - if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) { - if (orders.length === 1) { - adapter.recordOps = `@${snapshot}_rops`; - liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!)); - } else { - orders.forEach((order, i) => - liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)), - ); - adapter.recordOps = `@${snapshot}_rops`; - liveRecordOrderSelect(host, shape, snapshot); + if (!dirent && liveRecordOpsEligible(fields)) { + adapter.recordOps = `@${snapshot}_rops`; + liveRecordOps(host, t, snapshot, fields); + } + for (const field of fields) { + const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); + const fieldPtr = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, + ); + const fieldValue = loadStructField( + B, + host.sizeType, + fieldPtr, + field.type, + host.inlineUnions.record(field.type), + ); + // An ABSENT optional field contributes no key. + const undefinedTag = undefinedArmTag(field.type, host.unionsById); + let skip: string | null = null; + if (undefinedTag >= 0 && field.type.kind === "union") { + const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); + skip = B.newLabel("live.record.absent"); + const set = B.newLabel("live.record.set"); + B.condBr(absent, skip, set); + B.startBlock(set); + } + const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + B.line( + `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); } } - emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => { - for (const field of fieldsOf(order)) { - const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); - const fieldPtr = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, - ); - const fieldValue = loadStructField( - B, - host.sizeType, - fieldPtr, - field.type, - host.inlineUnions.record(field.type), - ); - // An ABSENT optional field contributes no key. - const undefinedTag = undefinedArmTag(field.type, host.unionsById); - let skip: string | null = null; - if (undefinedTag >= 0 && field.type.kind === "union") { - const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); - skip = B.newLabel("live.record.absent"); - const set = B.newLabel("live.record.set"); - B.condBr(absent, skip, set); - B.startBlock(set); - } - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); - B.line( - `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); - } - } - }); if (dirent) { host.declare(`declare void @scr_dyn_dirent_finish(ptr)`); B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`); @@ -1392,35 +1373,7 @@ function liveRecordOps( ``, `@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`, // Mutable: the runtime fills the trailing key-array slot on first use. - `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`, - ``, - ); -} - -/** `@_rops` for a shape with orderVariants: a record-ops table - * whose `select` hook answers the `_o_rops` table (liveRecordOps) of the - * record's current key order, which a write of an optional key can change - * after the record is boxed. */ -function liveRecordOrderSelect( - host: LlvmEmitterContext, - shape: IrRecordShape, - snapshot: string, -): void { - const B = new BlockBuilder(); - const out = B.slot(); - B.entryAllocas.push(`${out} = alloca ptr`); - emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => { - B.line(`store ptr @${snapshot}_o${i}_rops, ptr ${out}`); - }); - const ops = B.tmp(); - B.line(`${ops} = load ptr, ptr ${out}`); - B.terminate(`ret ptr ${ops}`); - host.resolveThunkDefs.push( - `define internal ptr @${snapshot}_select(ptr %p) ${FN_ATTRS} { ; live record ops by key order`, - B.render(), - `}`, - ``, - `@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`, + `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`, ``, ); } diff --git a/packages/compiler/src/backend/llvm/key-orders.ts b/packages/compiler/src/backend/llvm/key-orders.ts deleted file mode 100644 index 9dbda1414..000000000 --- a/packages/compiler/src/backend/llvm/key-orders.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* Key-order dispatch for shapes whose values record the order their keys - * were written in (IrRecordShape.orderVariants): an emitter that walks a - * record's keys emits one walk per order and branches on the value's - * KEY_ORDER_FIELD. */ -import type { IrRecordShape } from "../../ir/ir.js"; -import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js"; -import { mangleRecordStruct } from "../mangle.js"; -import type { BlockBuilder } from "./blocks.js"; - -/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a - * switch on the value's order index when the shape has several. Each walk - * starts in its own block, and the code after this call continues in the - * block they all branch to. */ -export function emitPerKeyOrder( - B: BlockBuilder, - shape: IrRecordShape, - fieldBase: number, - rec: string, - walk: (order: readonly string[], index: number) => void, -): void { - const orders = shapeKeyOrders(shape); - if (orders.length === 1) { - walk(orders[0]!, 0); - return; - } - const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase; - const p = B.tmp(); - const n = B.tmp(); - const i = B.tmp(); - B.line( - `${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`, - ); - B.line(`${n} = load double, ptr ${p}`); - B.line(`${i} = fptosi double ${n} to i32`); - const labels = orders.map(() => B.newLabel("ko")); - const join = B.newLabel("ko.join"); - B.terminate( - `switch i32 ${i}, label %${labels[0]} [ ${labels - .slice(1) - .map((label, k) => `i32 ${k + 1}, label %${label}`) - .join(" ")} ]`, - ); - orders.forEach((order, k) => { - B.startBlock(labels[k]!); - walk(order, k); - B.br(join); - }); - B.startBlock(join); -} diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 331ea2199..044cb79ae 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -1,4 +1,3 @@ -import { emitPerKeyOrder } from "./key-orders.js"; import { f64Lit } from "./common.js"; import { InternalCompilerError } from "../../errors.js"; /* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror @@ -329,80 +328,78 @@ export class LlWalkers { if (cyclic) this.jbLeave(B); return; } - // Fields serialize in DECLARED order (JS insertion order), or in the - // value's own order for a shape with orderVariants; internal + // Fields serialize in DECLARED order (JS insertion order); internal // '%'-fields stay hidden — jsonWriteHelper's contract, ported comment // and all. - emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => { - const inOrder = new Set(order); - if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { - throw new InternalCompilerError( - `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, + const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); + const inOrder = new Set(order); + if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { + throw new InternalCompilerError( + `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, + ); + } + const byName = new Map(shape.fields.map((f) => [f.name, f])); + // Function-valued properties are omitted, as JSON.stringify omits them. + const emitFields = order + .map((n) => byName.get(n)) + .filter((f) => f !== undefined) + .filter((f) => f.type.kind !== "func"); + const droppable = + emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || + !!shape.indexValue; + this.putc(B, "%b", "123"); // '{' + if (!droppable) { + emitFields.forEach((f, i) => { + this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, ); - } - const byName = new Map(shape.fields.map((f) => [f.name, f])); - // Function-valued properties are omitted, as JSON.stringify omits them. - const emitFields = order - .map((n) => byName.get(n)) - .filter((f) => f !== undefined) - .filter((f) => f.type.kind !== "func"); - const droppable = - emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || - !!shape.indexValue; - this.putc(B, "%b", "123"); // '{' - if (!droppable) { - emitFields.forEach((f, i) => { - this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); - }); - } else { - const first = B.slot(); - B.entryAllocas.push(`${first} = alloca i1`); - B.line(`store i1 true, ptr ${first}`); - const comma = (): void => { - const isf = B.tmp(); - const lc = B.newLabel("jwc.c"); - const lj = B.newLabel("jwc.j"); - B.line(`${isf} = load i1, ptr ${first}`); - B.condBr(isf, lj, lc); - B.startBlock(lc); - this.putc(B, "%b", "44"); // ',' - B.br(lj); - B.startBlock(lj); - B.line(`store i1 false, ptr ${first}`); - }; - for (const f of emitFields) { - const utag = undefinedArmTag(f.type, this.host.unionsById); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - let skip: string | null = null; - if (utag >= 0 && f.type.kind === "union") { - // Undefined-valued field: dropped, like Node. - const tag = this.unionTag(B, v, f.type.unionId); - const isu = B.tmp(); - B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); - const lw = B.newLabel("jwf.w"); - skip = B.newLabel("jwf.s"); - B.condBr(isu, skip, lw); - B.startBlock(lw); - } - comma(); - this.puts(B, "%b", jsonObjectKeyLabel(f.name)); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); - } + }); + } else { + const first = B.slot(); + B.entryAllocas.push(`${first} = alloca i1`); + B.line(`store i1 true, ptr ${first}`); + const comma = (): void => { + const isf = B.tmp(); + const lc = B.newLabel("jwc.c"); + const lj = B.newLabel("jwc.j"); + B.line(`${isf} = load i1, ptr ${first}`); + B.condBr(isf, lj, lc); + B.startBlock(lc); + this.putc(B, "%b", "44"); // ',' + B.br(lj); + B.startBlock(lj); + B.line(`store i1 false, ptr ${first}`); + }; + for (const f of emitFields) { + const utag = undefinedArmTag(f.type, this.host.unionsById); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + let skip: string | null = null; + if (utag >= 0 && f.type.kind === "union") { + // Undefined-valued field: dropped, like Node. + const tag = this.unionTag(B, v, f.type.unionId); + const isu = B.tmp(); + B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); + const lw = B.newLabel("jwf.w"); + skip = B.newLabel("jwf.s"); + B.condBr(isu, skip, lw); + B.startBlock(lw); + } + comma(); + this.puts(B, "%b", jsonObjectKeyLabel(f.name)); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); } - if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); } - }); + if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); + } this.putc(B, "%b", "125"); // '}' if (cyclic) this.jbLeave(B); } diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index cf3c632d7..1c362cb09 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -724,11 +724,6 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { - // Values of a shape with orderVariants enumerate in the order their - // literal wrote (key-order-witness.ts). - const mapped = safely(() => lowerer.mapTypeOf(contextual)); - if (mapped?.kind === "record" && lowerer.shapes.get(mapped.shapeId)?.orderVariants !== undefined) - return null; // A shape whose table the witness pass re-chose (key-order-witness.ts) // enumerates that table, not the checker's declaration order. const table = witnessTable(lowerer, contextual); 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 41398a1de..8632bad47 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -1,12 +1,5 @@ import { expect, test } from "vitest"; -import { - F64, - KEY_ORDER_FIELD, - type IrExpr, - type IrFunction, - type IrStmt, - type IrType, -} from "../../ir/ir.js"; +import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js"; import { ShapeRegistry } from "../type-mapper.js"; import { @@ -28,7 +21,7 @@ interface Fixture { notes: KeyOrderNotes; record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; - run(body: IrStmt[]): IrFunction; + run(body: IrStmt[]): void; order(type: IrType & { kind: "record" }): string[] | undefined; } @@ -42,14 +35,6 @@ function fixture(): Fixture { classes: new Map(), armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, - recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ - kind: "recordHas", - obj, - shapeId, - field, - type: { kind: "bool" }, - loc, - }), } as unknown as Lowerer; const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ kind: "record", @@ -70,20 +55,15 @@ function fixture(): Fixture { segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), ); }; - const run = (body: IrStmt[]): IrFunction => { + const run = (body: IrStmt[]): void => { const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; applyKeyOrderWitnesses(lowerer, [fn]); - return fn; }; const order = (type: IrType & { kind: "record" }): string[] | undefined => shapes.get(type.shapeId)?.declaredOrder; return { lowerer, shapes, notes, record, literal, run, order }; } -/** The order index a literal stores (KEY_ORDER_FIELD), when it stores one. */ -const orderIndexOf = (lit: IrExpr): IrExpr | undefined => - lit.kind === "recordLit" ? lit.fields.find((x) => x.name === KEY_ORDER_FIELD)?.value : undefined; - const observe = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, @@ -129,7 +109,7 @@ test("literals that agree with the table leave the same array in place", () => { expect(f.notes.changed.size).toBe(0); }); -test("values built in two orders each record their literal's order", () => { +test("values built in two orders keep today's table", () => { const f = fixture(); const ab = f.record( [ @@ -138,71 +118,8 @@ test("values built in two orders each record their literal's order", () => { ], ["a", "b"], ); - const ba = f.literal(ab, ["b", "a"]); - const aOrder = f.literal(ab, ["a", "b"]); - f.run([observe(ba), observe(aOrder)]); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); expect(f.order(ab)).toEqual(["a", "b"]); - const shape = f.shapes.get(ab.shapeId)!; - expect(shape.orderVariants).toEqual([ - ["a", "b"], - ["b", "a"], - ]); - expect(shape.fields.map((x) => x.name)).toEqual([KEY_ORDER_FIELD, "a", "b"]); - expect(orderIndexOf(ba)).toMatchObject({ kind: "numLit", value: 1 }); - expect(orderIndexOf(aOrder)).toMatchObject({ kind: "numLit", value: 0 }); -}); - -test("a copy of a value with orders stores its source's index", () => { - const f = fixture(); - const abc = f.record( - [ - ["a", F64], - ["b", F64], - ["c", F64], - ], - ["a", "b", "c"], - ); - const ab = f.record( - [ - ["a", F64], - ["b", F64], - ], - ["a", "b"], - ); - const read = (field: string): IrExpr => ({ - kind: "recordGet", - obj: ref("x", abc), - shapeId: abc.shapeId, - field, - type: F64, - loc, - }); - const rest = f.notes.copy( - { - kind: "recordLit", - fields: [ - { name: "a", value: read("a") }, - { name: "b", value: read("b") }, - ], - type: ab, - loc, - }, - abc.shapeId, - ); - f.run([ - decl("x", f.literal(abc, ["c", "b", "a"])), - observe(f.literal(abc, ["a", "b", "c"])), - observe(rest), - ]); - expect(f.shapes.get(ab.shapeId)!.orderVariants).toEqual([ - ["a", "b"], - ["b", "a"], - ]); - expect(orderIndexOf(rest)).toMatchObject({ - kind: "recordGet", - obj: { kind: "varRef", localId: "x" }, - field: KEY_ORDER_FIELD, - }); }); test("an unobserved shape keeps today's table", () => { @@ -420,7 +337,7 @@ test("straight-line writes of late-only keys keep program order", () => { expect(f.order(o)).toEqual(["id", "y", "x"]); }); -test("late-only keys written through different bindings move to the end per value", () => { +test("late-only keys written through different bindings keep today's table", () => { const f = fixture(); const o = f.record( [ @@ -437,13 +354,9 @@ test("late-only keys written through different bindings move to the end per valu observe(ref("o", o)), ]); expect(f.order(o)).toEqual(["id", "x", "y"]); - expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ - ["id", "x", "y"], - ["id", "y", "x"], - ]); }); -test("a late-only key written in a loop guards the write with its move", () => { +test("a late-only key written in a loop over an outer binding keeps today's table", () => { const f = fixture(); const o = f.record( [ @@ -453,7 +366,7 @@ test("a late-only key written in a loop guards the write with its move", () => { ], ["id", "x", "y"], ); - const body: IrStmt[] = [ + f.run([ decl("o", f.literal(o, ["id"])), set(ref("o", o), "y"), { @@ -463,18 +376,11 @@ test("a late-only key written in a loop guards the write with its move", () => { loc, }, observe(ref("o", o)), - ]; - const fn = f.run(body); + ]); expect(f.order(o)).toEqual(["id", "x", "y"]); - const loop = fn.body.find((st) => st.kind === "while") as IrStmt & { kind: "while" }; - const guarded = loop.body[0]!; - expect(guarded.kind).toBe("block"); - const [move, write] = (guarded as IrStmt & { kind: "block" }).body; - expect(move).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); - expect(write).toMatchObject({ kind: "recordSet", field: "x" }); }); -test("re-adding a key a literal creates earlier moves it to the end of that value", () => { +test("re-adding a key a literal creates earlier keeps today's table", () => { const f = fixture(); const o = f.record( [ @@ -493,119 +399,4 @@ test("re-adding a key a literal creates earlier moves it to the end of that valu }; f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]); expect(f.order(o)).toEqual(["a", "b"]); - expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ - ["a", "b"], - ["b", "a"], - ]); -}); - -test("a moving write evaluates its value before it tests and moves the key", () => { - const f = fixture(); - const o = f.record( - [ - ["id", F64], - ["x", OPT], - ["y", OPT], - ], - ["id", "x", "y"], - ); - // `o.x = ((o.y = 2), 3)`: y is created first. - const value: IrExpr = { - kind: "seqExpr", - stmts: [set(ref("o", o), "y")], - result: num(3), - type: F64, - loc, - }; - const write: IrStmt = { - kind: "recordSet", - obj: ref("o", o), - shapeId: o.shapeId, - field: "x", - value, - loc, - }; - const fn = f.run([ - decl("o", f.literal(o, ["id"])), - observe(f.literal(o, ["x", "id"])), - write, - observe(ref("o", o)), - ]); - const shape = f.shapes.get(o.shapeId)!; - expect(shape.orderVariants).toBeDefined(); - const block = fn.body[2] as IrStmt & { kind: "block" }; - expect(block.kind).toBe("block"); - // The receiver evaluates first, since the value may assign its binding. - const [receiver, first, guard, written] = block.body; - expect(receiver).toMatchObject({ kind: "varDecl", init: { kind: "varRef", localId: "o" } }); - const target = (receiver as IrStmt & { kind: "varDecl" }).localId; - expect(first).toMatchObject({ kind: "varDecl", init: { kind: "seqExpr" } }); - const local = (first as IrStmt & { kind: "varDecl" }).localId; - expect(guard).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); - expect(written).toMatchObject({ - kind: "recordSet", - obj: { kind: "varRef", localId: target }, - field: "x", - value: { kind: "varRef", localId: local }, - }); - // The write inside the value moves y in its own block. - const init = (first as IrStmt & { kind: "varDecl" }).init as IrExpr & { kind: "seqExpr" }; - expect(init.stmts[0]).toMatchObject({ kind: "block" }); -}); - -test("a key after a spread that the source may lack picks its order from the source's keys", () => { - const f = fixture(); - const s = f.record( - [ - ["a", F64], - ["b", OPT], - ["c", OPT], - ], - ["a", "b", "c"], - ); - const t = f.record( - [ - ["a", F64], - ["b", F64], - ["c", OPT], - ], - ["a", "b", "c"], - ); - const read = (field: string): IrExpr => ({ - kind: "recordGet", - obj: ref("x", s), - shapeId: s.shapeId, - field, - type: OPT, - loc, - }); - // `{ ...x, b: 1 }` - const merged = f.notes.literal( - { - kind: "recordLit", - fields: [ - { name: "a", value: read("a") }, - { name: "c", value: read("c") }, - { name: "b", value: num(1) }, - ], - type: t, - loc, - }, - [ - { kind: "spread", shapeId: s.shapeId, maybeAbsent: false }, - { kind: "key", name: "b", definite: true }, - ], - ); - f.run([decl("x", f.literal(s, ["c", "a"])), observe(f.literal(s, ["a", "c"])), observe(merged)]); - // b keeps its place in the source's order when the source has it, and - // follows the source's keys when it does not. - expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([ - ["a", "c", "b"], - ["c", "a", "b"], - ["a", "b", "c"], - ]); - expect(orderIndexOf(merged)).toMatchObject({ - kind: "ternary", - cond: { kind: "recordHas", obj: { kind: "varRef", localId: "x" }, field: "b" }, - }); }); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 9cbbd34a2..4adfb87ab 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -41,57 +41,12 @@ * value: that shape, and every shape copied from it, keeps today's table * (and today's SC6002 warnings). Shapes that no order surface observes * keep today's table too. */ -import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; -import { - BOOL, - F64, - KEY_ORDER_FIELD, - shapeHasAccessorSlots, - typeEquals, - UNDEFINED_T, -} from "../../ir/ir.js"; -import { - everyExprChild, - everyStmtChild, - everyTypeChild, - transformStmtList, -} from "../../ir/traverse.js"; +import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js"; +import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.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 - * branch per order (walks[i] for order i) on the value's KEY_ORDER_FIELD. - * `walks` follows shapeKeyOrders. */ -export function perKeyOrder( - shape: IrRecordShape, - ref: IrExpr, - walks: readonly IrStmt[][], - loc: SrcLoc, -): IrStmt[] { - let tail = walks[0]!; - for (let i = walks.length - 1; i >= 1; i--) { - const index: IrExpr = { - kind: "recordGet", - obj: ref, - shapeId: shape.id, - field: KEY_ORDER_FIELD, - type: F64, - loc, - }; - const cond: IrExpr = { - kind: "bin", - op: "===", - left: index, - right: { kind: "numLit", value: i, type: F64, loc }, - type: BOOL, - loc, - }; - tail = [{ kind: "if", cond, then: walks[i]!, else_: tail, loc }]; - } - return tail; -} - /** One property of an object literal, in source order. */ export type KeyOrderSegment = /** An explicit key (`definite`), or a conditional spread's key, which @@ -758,496 +713,4 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; notes.changed.add(id); } - - // ── 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 - // in (KEY_ORDER_FIELD), when every construction's order is known - // statically or copied from a value that records its own: - // - a literal of explicit keys writes its order; - // - a copy from a shape with one resolved order writes that order; - // - a copy from a value of a shape with orders writes the source's - // order, filtered to the copy's keys (rest bindings, width copies); - // - clones copy the field, and every other construction (synthesized - // literals, checked conversions, runtime producers) leaves 0, today's - // table, as before. - // A write of an optional key a value lacks moves the key to the end of - // the value's order, as in Node: the orders include every order such - // writes reach, and each write site updates the index after it evaluates - // the value and before it writes the key. - const MAX_ORDERS = 16; - const unresolved = (id: string): boolean => result.get(id) === "unknown"; - /** `order` over every key of `table`: keys the order lacks follow it. */ - const complete = (order: readonly string[], table: readonly string[]): string[] => { - const keys = new Set(table); - const out = order.filter((k) => keys.has(k)); - const seen = new Set(out); - return [...out, ...table.filter((k) => !seen.has(k))]; - }; - const literalOrder = (segments: readonly KeyOrderSegment[]): string[] | null => { - const out: string[] = []; - for (const segment of segments) { - if (segment.kind !== "key" || !segment.definite) return null; - if (!isArrayIndex(segment.name)) out.push(segment.name); - } - return out; - }; - /** `{ ...value }` of a value of the literal's own shape: a copy of the - * value's order. */ - const selfSpread = (segments: readonly KeyOrderSegment[], id: string): boolean => - segments.length === 1 && - segments[0]!.kind === "spread" && - segments[0]!.shapeId === id && - !segments[0]!.maybeAbsent; - const sameOrder = (a: readonly string[], b: readonly string[]): boolean => - a.join("\0") === b.join("\0"); - const indexOfOrder = (orders: readonly string[][], order: readonly string[]): number => - orders.findIndex((o) => sameOrder(o, order)); - /** The keys writes can add to a value of the shape, or null when a - * clone override can (the clone's order is its source's). */ - const movingKeys = (id: string, f: ShapeFacts, table: readonly string[]): string[] | null => { - const keys = new Set(table); - const moving = new Set(); - for (const w of f.writes) { - if (w.kind === "delete" || !keys.has(w.field) || !maybeAbsent(id, w.field)) continue; - if (w.kind === "clone") return null; - moving.add(w.field); - } - return [...moving]; - }; - /** `values[i]` for the order index `current` holds, as a chain of - * tests. */ - const indexChain = (current: IrExpr, values: readonly number[], loc: SrcLoc): IrExpr => { - let out: IrExpr = { kind: "numLit", value: values[values.length - 1]!, type: F64, loc }; - for (let i = values.length - 2; i >= 0; i--) { - if (out.kind === "numLit" && out.value === values[i]) continue; - out = { - kind: "ternary", - cond: { - kind: "bin", - op: "===", - left: current, - right: { kind: "numLit", value: i, type: F64, loc }, - type: BOOL, - loc, - }, - then: { kind: "numLit", value: values[i]!, type: F64, loc }, - else_: out, - type: F64, - loc, - }; - } - return out; - }; - const moves = new Map>(); - /** Adds to `orders` every order writes of `moving` keys reach from them - * and records each key's transitions; false past MAX_ORDERS. */ - const closeUnderMoves = (id: string, orders: string[][], moving: readonly string[]): boolean => { - const transitions = new Map(moving.map((k) => [k, []])); - for (let i = 0; i < orders.length; i++) { - for (const key of moving) { - const moved = [...orders[i]!.filter((k) => k !== key), key]; - let index = indexOfOrder(orders, moved); - if (index < 0) { - if (orders.length === MAX_ORDERS) return false; - index = orders.push(moved) - 1; - } - transitions.get(key)![i] = index; - } - } - if (moving.length > 0) moves.set(id, transitions); - return true; - }; - // Observed shapes, and the shapes their values copy their orders from. - const wanted = new Set(observed); - const pending = [...observed]; - while (pending.length > 0) { - const f = facts.get(pending.pop()!); - if (f === undefined) continue; - const sources = [ - ...f.copies.map((c) => c.source), - ...f.literals.flatMap((segments) => - segments.flatMap((s) => (s.kind === "spread" ? [s.shapeId] : [])), - ), - ]; - for (const source of sources) { - if (wanted.has(source)) continue; - wanted.add(source); - pending.push(source); - } - } - const orderable = ( - id: string, - f: ShapeFacts, - ): { shape: IrRecordShape; moving: string[] } | null => { - const shape = shapes.get(id); - if (!wanted.has(id) || !unresolved(id) || !inScope(shape)) return null; - const moving = movingKeys(id, f, tableOf(shape)); - return moving === null ? null : { shape, moving }; - }; - const variants = new Map(); - for (const [id, f] of facts) { - const candidate = orderable(id, f); - if (candidate === null || f.literals.length === 0) continue; - const table = tableOf(candidate.shape); - const orders: string[][] = [table]; - let ok = true; - const add = (order: readonly string[]): void => { - const full = complete(order, table); - if (indexOfOrder(orders, full) < 0) orders.push(full); - }; - for (const segments of f.literals) { - if (selfSpread(segments, id)) continue; - const order = literalOrder(segments); - if (order === null) ok = false; - else add(order); - } - for (const c of f.copies) { - if (c.source === id) continue; - const source = result.get(c.source); - if (source === undefined || source === "unknown") ok = false; - else add(source.order.filter((k) => c.keys.has(k))); - } - if (!ok || orders.length > MAX_ORDERS || !closeUnderMoves(id, orders, candidate.moving)) - continue; - if (orders.length > 1) variants.set(id, orders); - else moves.delete(id); - } - // Shapes built only from values of one shape with orders, all in one - // way: copies (rest bindings, width copies), or literals spreading the - // value between the same explicit keys. Order i is the source's order i - // over the shape's keys, after the keys before the spread and before - // the keys after it. A key after the spread that the source value may - // lack goes where the source has it, or after the source's keys when it - // has not: such a literal's orders are one per source order and set of - // those keys the source has, and `select.index[present][i]` is the order - // for source order i with the keys of bit set `present`. - type Template = { - source: string; - prefix: string[]; - suffix: string[]; - select?: { keys: string[]; index: number[][] }; - }; - const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => { - const prefix: string[] = []; - const suffix: string[] = []; - let source: string | null = null; - for (const segment of segments) { - if (segment.kind === "opaque") return null; - if (segment.kind === "spread") { - if (source !== null || segment.maybeAbsent || segment.shapeId === id) return null; - source = segment.shapeId; - continue; - } - if (!segment.definite) return null; - (source === null ? prefix : suffix).push(segment.name); - } - return source === null ? null : { source, prefix, suffix }; - }; - const templateKey = (t: Template): string => JSON.stringify([t.source, t.prefix, t.suffix]); - const copyTemplate = (source: string): Template => ({ source, prefix: [], suffix: [] }); - const mirrors = new Map(); - for (const [id, f] of facts) { - if (variants.has(id) || f.pinned || f.runtime || f.opaque) continue; - const candidate = orderable(id, f); - if (candidate === null || f.literals.length + f.copies.length === 0) continue; - const templates: (Template | null)[] = f.literals.map((segments) => templateOf(segments, id)); - for (const c of f.copies) templates.push(copyTemplate(c.source)); - const first = templates[0]; - if ( - first === null || - first === undefined || - templates.some((t) => t === null || templateKey(t) !== templateKey(first)) - ) - continue; - const orders = variants.get(first.source); - if (orders === undefined || mirrors.has(first.source)) continue; - const table = tableOf(candidate.shape); - const tested = first.suffix.filter((k) => maybeAbsent(first.source, k)); - if (tested.length > 4) continue; - const mirrorOf = (o: readonly string[], lacks: ReadonlySet): string[] => { - const order = [...first.prefix]; - for (const k of o) if (!lacks.has(k) && !order.includes(k)) order.push(k); - for (const k of first.suffix) if (!order.includes(k)) order.push(k); - return complete(order, table); - }; - let mirrored: string[][]; - let template: Template = first; - if (tested.length === 0) mirrored = orders.map((o) => mirrorOf(o, new Set())); - else { - mirrored = []; - const index: number[][] = []; - for (let present = 0; present < 1 << tested.length; present++) { - const lacks = new Set(tested.filter((_, j) => (present & (1 << j)) === 0)); - const row: number[] = []; - for (const o of orders) { - const order = mirrorOf(o, lacks); - let i = indexOfOrder(mirrored, order); - if (i < 0) i = mirrored.push(order) - 1; - row.push(i); - } - index.push(row); - } - if (mirrored.length > MAX_ORDERS) continue; - template = { - source: first.source, - prefix: first.prefix, - suffix: first.suffix, - select: { keys: tested, index }, - }; - } - if (!closeUnderMoves(id, mirrored, candidate.moving)) continue; - if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) { - // Every order of the source agrees over these keys: one table. - moves.delete(id); - const shape = candidate.shape; - const only: string[] = mirrored[0] ?? table; - if (!sameOrder(only, table)) - shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...only]; - continue; - } - variants.set(id, mirrored); - mirrors.set(id, template); - } - if (variants.size === 0) return; - - // Every construction of those shapes, to give each its order index. A - // copy reads its source's index, so its source must be a plain binding. - const sites = new Map(); - /** The statements a spread literal's sequence evaluates first: the - * spread value's binding, then its fields. */ - const prologues = new Map(); - const siteOf = (e: IrExpr): boolean => { - if (e.kind === "seqExpr" && e.result.kind === "recordLit") prologues.set(e.result, e.stmts); - if (e.kind === "recordLit" && e.type.kind === "record" && variants.has(e.type.shapeId)) { - const list = sites.get(e.type.shapeId) ?? []; - list.push(e); - sites.set(e.type.shapeId, list); - } - return everyExprChild(e, siteOf, siteStmt); - }; - const siteStmt = (st: IrStmt): boolean => everyStmtChild(st, siteOf, siteStmt); - for (const fn of fns) for (const st of fn.body) siteStmt(st); - /** The binding a copy or spread reads its fields from. */ - const copySource = (lit: IrExpr & { kind: "recordLit" }, source: string): IrExpr | null => { - for (const field of lit.fields) { - const value = field.value; - if (value.kind === "recordGet" && value.shapeId === source && value.obj.kind === "varRef") - return value.obj; - } - for (const st of prologues.get(lit) ?? []) { - if (st.kind !== "varDecl" || st.init === null) continue; - const t = st.init.type; - if (t.kind === "record" && t.shapeId === source) - return { kind: "varRef", localId: st.localId, type: t, loc: lit.loc }; - } - return null; - }; - const orderIndex = (id: string, lit: IrExpr & { kind: "recordLit" }): IrExpr | number | null => { - const orders = variants.get(id)!; - const segments = notes.literalOf(lit); - if (segments !== undefined && selfSpread(segments, id)) { - const obj = copySource(lit, id); - if (obj === null) return null; - return { - kind: "recordGet", - obj, - shapeId: id, - field: KEY_ORDER_FIELD, - type: F64, - loc: lit.loc, - }; - } - const mirror = mirrors.get(id); - if (segments !== undefined && mirror !== undefined) { - const obj = copySource(lit, mirror.source); - if (obj === null) return null; - const sourceIndex: IrExpr = { - kind: "recordGet", - obj, - shapeId: mirror.source, - field: KEY_ORDER_FIELD, - type: F64, - loc: lit.loc, - }; - const select = mirror.select; - if (select === undefined) return sourceIndex; - // Tests the source for each key in turn, then picks by its index. - const pick = (j: number, present: number): IrExpr => { - if (j === select.keys.length) - return indexChain(sourceIndex, select.index[present]!, lit.loc); - return { - kind: "ternary", - cond: lowerer.recordFieldPresent(obj, mirror.source, select.keys[j]!, lit.loc), - then: pick(j + 1, present | (1 << j)), - else_: pick(j + 1, present), - type: F64, - loc: lit.loc, - }; - }; - return pick(0, 0); - } - if (segments !== undefined) { - const order = literalOrder(segments); - return order === null ? null : indexOfOrder(orders, complete(order, orders[0]!)); - } - const source = notes.copyOf(lit); - if (source === undefined) return 0; - if (variants.has(source) && (source === id || mirrors.get(id)?.source === source)) { - const obj = copySource(lit, source); - if (obj === null) return null; - return { - kind: "recordGet", - obj, - shapeId: source, - field: KEY_ORDER_FIELD, - type: F64, - loc: lit.loc, - }; - } - const resolved = result.get(source); - if (resolved === undefined || resolved === "unknown") return null; - const keys = new Set( - lit.fields.filter((x) => x.value.kind !== "fieldAbsent").map((x) => x.name), - ); - return indexOfOrder( - orders, - complete( - resolved.order.filter((k) => keys.has(k)), - orders[0]!, - ), - ); - }; - const plan = new Map(); - const dropped = new Set(); - for (const [id, list] of sites) { - for (const lit of list) { - const index = orderIndex(id, lit); - if (index === null || (typeof index === "number" && index < 0)) dropped.add(id); - else plan.set(lit, index); - } - } - // Write sites of keys that move: each reads and updates the value's - // index before the write. - const writeSites: { st: IrStmt & { kind: "recordSet" }; fn: IrFunction }[] = []; - let writeFn: IrFunction | null = null; - const writeExpr = (e: IrExpr): boolean => everyExprChild(e, writeExpr, writeOf); - const writeOf = (st: IrStmt): boolean => { - if ( - st.kind === "recordSet" && - st.value.kind !== "fieldAbsent" && - moves.get(st.shapeId)?.has(st.field) - ) - writeSites.push({ st, fn: writeFn! }); - return everyStmtChild(st, writeExpr, writeOf); - }; - for (const fn of fns) { - writeFn = fn; - for (const st of fn.body) writeOf(st); - } - // A shape whose copies cannot read their source keeps today's table, and - // so do the shapes that mirror it. - for (const [id, mirror] of mirrors) if (dropped.has(mirror.source)) dropped.add(id); - for (const [lit, index] of plan) { - const id = (lit.type as IrType & { kind: "record" }).shapeId; - if (dropped.has(id)) continue; - const value: IrExpr = - typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index; - lit.fields.push({ name: KEY_ORDER_FIELD, value }); - } - let temporaries = 0; - const replacements = new Map>(); - for (const { st, fn } of writeSites) { - if (dropped.has(st.shapeId)) continue; - const transitions = moves.get(st.shapeId)!.get(st.field)!; - const loc = st.loc; - // A receiver other than a binding evaluates once, into a local. So does - // a binding when the value evaluates first (below): the value may - // assign the binding, and the write still goes to the object the - // binding held before. - const pure = - st.value.kind === "numLit" || - st.value.kind === "strLit" || - st.value.kind === "boolLit" || - st.value.kind === "varRef"; - let plain = st.obj; - while (plain.kind === "unionNarrow" || plain.kind === "downcast") plain = plain.value; - const prologue: IrStmt[] = []; - let obj = st.obj; - if (plain.kind !== "varRef" || !pure) { - const localId = `%keyOrderReceiver.${temporaries++}`; - fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false }); - prologue.push({ kind: "varDecl", localId, init: st.obj, loc }); - obj = { kind: "varRef", localId, type: st.obj.type, loc }; - } - const current: IrExpr = { - kind: "recordGet", - obj, - shapeId: st.shapeId, - field: KEY_ORDER_FIELD, - type: F64, - loc, - }; - const next = indexChain(current, transitions, loc); - // The value evaluates before the key is tested and moved: it may add - // other keys to the same value, which then come first. - let value = st.value; - if (!pure) { - const localId = `%keyOrderValue.${temporaries++}`; - fn.locals.push({ id: localId, name: "%keyOrderValue", type: value.type, mutable: false }); - prologue.push({ kind: "varDecl", localId, init: value, loc }); - value = { kind: "varRef", localId, type: value.type, loc }; - } - const absent: IrExpr = { - kind: "unary", - op: "!", - operand: lowerer.recordFieldPresent(obj, st.shapeId, st.field, loc), - type: BOOL, - loc, - }; - const update: IrStmt = { - kind: "recordSet", - obj, - shapeId: st.shapeId, - field: KEY_ORDER_FIELD, - value: next, - loc, - }; - // The write becomes a block that moves the key first. - const write: IrStmt = { - kind: "recordSet", - obj, - shapeId: st.shapeId, - field: st.field, - value, - loc, - }; - const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc }; - const block: IrStmt = { kind: "block", body: [...prologue, guard, write], loc }; - let replaced = replacements.get(fn); - if (replaced === undefined) { - replaced = new Map(); - replacements.set(fn, replaced); - } - replaced.set(st, block); - } - for (const [fn, replaced] of replacements) { - fn.body = transformStmtList(fn.body, { - expr: (e) => e, - stmt: (st) => replaced.get(st) ?? st, - }); - } - for (const [id, orders] of variants) { - if (dropped.has(id)) continue; - const shape = shapes.get(id)!; - const indexKeys: string[] = (shape.declaredOrder ?? []).filter(isArrayIndex); - shapes.addKeyOrderField(id); - const tables: string[][] = []; - for (const order of orders) { - const table: string[] = [...indexKeys]; - for (const key of order) table.push(key); - tables.push(table); - } - shape.orderVariants = tables; - } } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 02fd008a8..c0406e77b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,5 +1,4 @@ import { keyedHelperName } from "./stable-names.js"; -import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -74,7 +73,6 @@ import { isDynTypedRefType, isUnitType, shapeHasAccessorSlots, - shapeKeyOrders, typeEquals, RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; @@ -7107,12 +7105,8 @@ export function recordKeysArrayCall( loc, }, ]; - // One walk per key order (see perKeyOrder). - const orders = shapeKeyOrders(current); - const walks = orders.map((): IrStmt[] => []); - for (const [k, name] of orders.flatMap((order, k) => - order.map((name) => [k, name] as const), - )) { + const order = current.declaredOrder ?? current.fields.map((f) => f.name); + for (const name of order) { const f = current.fields.find((x) => x.name === name)!; const pushStmt: IrStmt = { kind: "exprStmt", @@ -7129,13 +7123,12 @@ export function recordKeysArrayCall( // Fields that can be absent push behind their presence test (the // key exists exactly when Object.keys would list it). const present = lowerer.recordFieldPresent(ref, argIr.shapeId, f.name, loc); - walks[k]!.push( + body.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [pushStmt], else_: null, loc } : pushStmt, ); } - body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; @@ -8821,12 +8814,8 @@ function lowerObjectStaticCall( loc, }, ]; - // One walk per key order (see perKeyOrder). - const orders = shapeKeyOrders(current); - const walks = orders.map((): IrStmt[] => []); - for (const [k, name] of orders.flatMap((order, k) => - order.map((name) => [k, name] as const), - )) { + const order = current.declaredOrder ?? current.fields.map((f) => f.name); + for (const name of order) { const f = current.fields.find((x) => x.name === name)!; const raw: IrExpr = { kind: "recordGet", @@ -8988,9 +8977,8 @@ function lowerObjectStaticCall( loc, }; } - walks[k]!.push(stmt); + body.push(stmt); } - body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 57f16bcc7..47f490c7b 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,5 +1,4 @@ import { keyedHelperName } from "./stable-names.js"; -import { perKeyOrder } from "./key-order-witness.js"; import { dynUndefinedExpr, boolLit, @@ -41,7 +40,6 @@ import { isDynTypedRefType, recordTextCodecClass, shapeHasAccessorSlots, - shapeKeyOrders, typeKey, } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; @@ -1649,12 +1647,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { }); } body.push(depthGate("[Object]"), ...begin()); - // One walk per key order (see perKeyOrder). - const orders = shapeKeyOrders(current); - const walks = orders.map((): IrStmt[] => []); - for (const [k, fname] of orders.flatMap((order, k) => - order.map((name) => [k, name] as const), - )) { + for (const fname of order) { const ft = byName.get(fname); if (!ft) continue; const show = entry( @@ -1662,13 +1655,12 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { boolLit(false, loc), ); const present = lowerer.recordFieldPresent(v(), t.shapeId, fname, loc); - walks[k]!.push( + body.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [show], else_: null, loc } : show, ); } - body.push(...perKeyOrder(current, v(), walks, loc)); body.push( ret(end(strLit("", loc), strLit("{", loc), strLit("}", loc), false, boolLit(false, loc))), ); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 78efe3d17..7a7c165a8 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -43,7 +43,7 @@ import { UNDEFINED_T, VOID, } from "../ir/ir.js"; -import { BIGINT_T, KEY_ORDER_FIELD } from "../ir/ir.js"; +import { BIGINT_T } from "../ir/ir.js"; import { literalValues } from "./literal-values.js"; import { remapUnionDiscriminant } from "./union-discriminants.js"; @@ -555,21 +555,6 @@ export class ShapeRegistry { return missing.length === 0 ? order : [...order, ...missing]; } - /** Gives a shape with orderVariants its KEY_ORDER_FIELD, in place, - * after the bodies lowered (key-order-witness.ts adds the field to each - * of the shape's literals). Allocation zeroes it, which every other - * construction keeps. */ - addKeyOrderField(shapeId: string): void { - const shape = this.byId.get(shapeId); - if (!shape || shape.fields.some((f) => f.name === KEY_ORDER_FIELD)) return; - shape.fields = [...shape.fields, { name: KEY_ORDER_FIELD, type: F64 }].sort((a, b) => - a.name < b.name ? -1 : a.name > b.name ? 1 : 0, - ); - this.revision++; - const key = this.keyOf(shape.fields, false, shape.indexValue); - if (!this.byKey.has(key)) this.byKey.set(key, shapeId); - } - /** Adds optional fields to an interned shape in place: members a type * assertion attaches to the shape's values (`r as R & { tag: string }` * followed by `view.tag = ...`; see assertion-members.ts). Every value diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 0a802c03d..7bef40aa6 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -172,7 +172,6 @@ const MODULE_FIELDS: Record>, Role> = { "records[].fields[].name": "other", "records[].declaredOrder[]": "other", "records[].accessorOrder[]": "other", - "records[].orderVariants[][]": "other", "unions[].id": "other", "unions[].discriminant.field": "other", "unions[].discriminant.cases[].values[]": "other", diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index cb0c7e496..12400fee9 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1691,24 +1691,6 @@ export interface IrRecordShape { * an accessor property named once (see accessorSlotProp). The * checked-dynamic copy lists its keys in this order. */ accessorOrder?: string[]; - /** Every order the shape's values enumerate their keys in, when its - * object literals write the keys in more than one order (see - * key-order-witness.ts); declaredOrder is the first. Each value holds the - * index of its order in the hidden number field KEY_ORDER_FIELD: the - * literal that wrote it stores its order's index, a copy its source's, - * and every other construction leaves 0. Each key-order surface - * dispatches on that field. */ - orderVariants?: string[][]; -} - -/** The hidden field of a shape with orderVariants: the index of the value's - * key order. */ -export const KEY_ORDER_FIELD = "%order"; - -/** The orders a record shape's values enumerate their keys in: each order - * of a shape with orderVariants, or its one table. */ -export function shapeKeyOrders(shape: IrRecordShape): readonly (readonly string[])[] { - return shape.orderVariants ?? [shape.declaredOrder ?? shape.fields.map((f) => f.name)]; } /** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json deleted file mode 100644 index 2609ff929..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-key-order-json.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7132-key-order-json.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json deleted file mode 100644 index 40729240a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7133-key-order-keys-values-entries.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7133-key-order-keys-values-entries.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json deleted file mode 100644 index e44503626..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7134-key-order-for-in.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7134-key-order-for-in.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json deleted file mode 100644 index 431bf5c9c..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7135-key-order-inspect.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7135-key-order-inspect.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json deleted file mode 100644 index fea64a145..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7136-key-order-copies.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7136-key-order-copies.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json deleted file mode 100644 index 3b7cab36f..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7137-key-order-dynamic.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7137-key-order-dynamic.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json deleted file mode 100644 index 7f5302cd5..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7138-key-order-late-writes.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7138-key-order-late-writes.ts" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index b2e4a5de7..74b710dc9 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -363,12 +363,6 @@ typedef struct ScrDynTypedRecordOps { /* The field names as an immortal string[], built on first use by * for-in over a record with every field present (atomic slot). */ ScrArr *keys; - /* A shape whose values enumerate their keys in more than one order - * (IrRecordShape.orderVariants) binds a table with only this hook: it - * answers the table of the record's current order, which a write of an - * optional key can change after the record is boxed. NULL in every other - * table. */ - const struct ScrDynTypedRecordOps *(*select)(void *rec); } ScrDynTypedRecordOps; /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index bba30a5db..cf658313f 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3595,8 +3595,7 @@ bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) { static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) { if (!d->v.typed_ref.record || d->v.typed_ref.materialized) return NULL; - const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; - return ops && ops->select ? ops->select(d->v.typed_ref.ptr) : ops; + return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; } const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) { diff --git a/tests/corpus/7132-key-order-json.ts b/tests/corpus/7132-key-order-json.ts deleted file mode 100644 index 0cd3dff61..000000000 --- a/tests/corpus/7132-key-order-json.ts +++ /dev/null @@ -1,31 +0,0 @@ -// JSON.stringify of object literals that write the same keys in different -// orders: each value serializes in the order its literal wrote, as in Node. -type Point = { x: number; y: number; label?: string }; - -const a = { id: 1, name: "a", ok: true }; -const b = { name: "b", ok: false, id: 2 }; -const c = { ok: true, id: 3, name: "c" }; -for (const value of [a, b, c]) console.log(JSON.stringify(value)); -console.log(JSON.stringify([a, b, c])); -console.log(JSON.stringify({ first: b, rest: [c, a] }, null, 2)); - -// Optional keys the literal leaves out, and writes to keys it has. -const p: Point = { y: 2, x: 1 }; -const q: Point = { label: "q", x: 3, y: 4 }; -const r: Point = { x: 5, y: 6, label: "r" }; -q.x = 30; -r.label = "r2"; -for (const point of [p, q, r]) console.log(JSON.stringify(point)); - -// A function typed by one literal's shape receiving the others. -function describe(value: { id: number; name: string; ok: boolean }): string { - return JSON.stringify(value); -} -console.log(describe(c), describe(a)); - -// JSON.parse into the same record type keeps each text's own key order. -type Entry = { id: number; name: string; ok: boolean }; -for (const text of ['{"ok":true,"name":"p","id":7}', '{"id":8,"name":"q","ok":false}']) { - const parsed = JSON.parse(text) as Entry; - console.log(JSON.stringify(parsed), Object.keys(parsed).join(","), describe(parsed)); -} diff --git a/tests/corpus/7133-key-order-keys-values-entries.ts b/tests/corpus/7133-key-order-keys-values-entries.ts deleted file mode 100644 index 29f725c24..000000000 --- a/tests/corpus/7133-key-order-keys-values-entries.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Object.keys, Object.values, Object.entries and Object.fromEntries over -// object literals that write the same keys in different orders. -const a = { id: 1, name: "a", ok: true }; -const b = { name: "b", ok: false, id: 2 }; -const c = { ok: true, id: 3, name: "c" }; -for (const value of [a, b, c]) { - console.log(Object.keys(value).join(",")); - console.log(Object.values(value).join(",")); - console.log(Object.entries(value).map(([key, item]) => `${key}=${String(item)}`).join(" ")); - console.log(JSON.stringify(Object.fromEntries(Object.entries(value)))); -} - -type Row = { year: number; title?: string; score: number }; -const rows: Row[] = [ - { score: 1, year: 2001 }, - { title: "t", year: 2002, score: 2 }, - { year: 2003, score: 3, title: "u" }, -]; -for (const row of rows) console.log(Object.keys(row).join(","), Object.values(row).length); diff --git a/tests/corpus/7134-key-order-for-in.ts b/tests/corpus/7134-key-order-for-in.ts deleted file mode 100644 index 89ed16edb..000000000 --- a/tests/corpus/7134-key-order-for-in.ts +++ /dev/null @@ -1,15 +0,0 @@ -// for-in over object literals that write the same keys in different -// orders, and over values a function received. -const a = { id: 1, name: "a", ok: true }; -const b = { name: "b", ok: false, id: 2 }; -const c = { ok: true, id: 3, name: "c" }; -function keysOf(value: { id: number; name: string; ok: boolean }): string { - const keys: string[] = []; - for (const key in value) keys.push(key); - return keys.join(","); -} -for (const value of [a, b, c]) { - const seen: string[] = []; - for (const key in value) seen.push(key.toUpperCase()); - console.log(seen.join(" "), keysOf(value)); -} diff --git a/tests/corpus/7135-key-order-inspect.ts b/tests/corpus/7135-key-order-inspect.ts deleted file mode 100644 index 6a11eaf2c..000000000 --- a/tests/corpus/7135-key-order-inspect.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { inspect } from "node:util"; - -// console.log and util.inspect of object literals that write the same keys -// in different orders, alone and nested. -const a = { id: 1, name: "a", ok: true }; -const b = { name: "b", ok: false, id: 2 }; -const c = { ok: true, id: 3, name: "c" }; -console.log(a, b, c); -console.log([a, b, c]); -console.log({ outer: { inner: b }, list: [c] }); -console.log(inspect(b, { depth: 0 }), inspect({ deep: { deeper: c } }, { depth: 0 })); -console.log(`${inspect(c)} / ${inspect(a)}`); diff --git a/tests/corpus/7136-key-order-copies.ts b/tests/corpus/7136-key-order-copies.ts deleted file mode 100644 index 5d754bced..000000000 --- a/tests/corpus/7136-key-order-copies.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copies of object literals that write the same keys in different orders: -// spreads, spreads with other keys before and after, rest bindings, -// Object.assign and structuredClone keep each value's own order. -const a = { id: 1, name: "a", ok: true }; -const b = { name: "b", ok: false, id: 2 }; -const c = { ok: true, id: 3, name: "c" }; -for (const value of [a, b, c]) { - const copy = { ...value }; - const changed = { ...value, id: 10 }; - const after = { ...value, extra: "e" }; - const before = { first: 0, ...value }; - const { name, ...rest } = value; - const assigned = Object.assign({}, value); - const cloned = structuredClone(value); - console.log(JSON.stringify(copy), JSON.stringify(changed)); - console.log(JSON.stringify(after), Object.keys(before).join(",")); - console.log(name, JSON.stringify(rest), Object.keys(rest).join(",")); - console.log(JSON.stringify(assigned), JSON.stringify(cloned)); -} -// Keys after a spread that the spread value may lack: a key the value has -// keeps its place, and a key it lacks goes after the value's keys. -type Options = { id: number; label?: string; size?: number; tag?: string }; -const options: Options[] = [ - { id: 1, size: 2 }, - { size: 3, id: 2 }, - { label: "c", id: 3, tag: "t" }, - { tag: "u", size: 4, id: 4 }, -]; -for (const value of options) { - const merged = { ...value, tag: "x", label: "y", done: true }; - console.log(Object.keys(merged).join(","), JSON.stringify(merged)); -} diff --git a/tests/corpus/7137-key-order-dynamic.ts b/tests/corpus/7137-key-order-dynamic.ts deleted file mode 100644 index 55970cb40..000000000 --- a/tests/corpus/7137-key-order-dynamic.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MessageChannel } from "node:worker_threads"; - -// Object literals that write the same keys in different orders, seen -// through untyped values: their keys, JSON text, own property names, and a -// MessageChannel message. -const a = { id: 1, name: "a", ok: true }; -const b = { name: "b", ok: false, id: 2 }; -const c = { ok: true, id: 3, name: "c" }; -const values: unknown[] = [a, b, c]; -for (const value of values) { - const object = value as Record; - console.log(Object.keys(object).join(","), JSON.stringify(object)); - console.log(Object.getOwnPropertyNames(object).join(",")); -} -const { port1, port2 } = new MessageChannel(); -let received = 0; -port1.on("message", (message: unknown) => { - console.log("message", JSON.stringify(message), Object.keys(message as object).join(",")); - if (++received === 3) port1.close(); -}); -for (const value of [a, b, c]) port2.postMessage(value); - -// A nested value seen through an untyped view keeps its identity and its -// own key order, and writes through the view reach it. -const holder = { inner: b, tag: "h" }; -const view = holder as unknown as Record; -const inner = view.inner as Record; -console.log(view.inner === view.inner, Object.keys(inner).join(",")); -inner.id = 20; -console.log(b.id, JSON.stringify(b), JSON.stringify(view)); diff --git a/tests/corpus/7138-key-order-late-writes.ts b/tests/corpus/7138-key-order-late-writes.ts deleted file mode 100644 index 4166999d7..000000000 --- a/tests/corpus/7138-key-order-late-writes.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Optional keys added after construction go last, as in Node, also for -// shapes whose literals write their keys in different orders: a write of a -// missing key, a delete and a later write, and writes in a loop. -type Item = { id: number; name?: string; note?: string; count: number }; - -const items: Item[] = [ - { id: 1, count: 0 }, - { count: 1, name: "b", id: 2 }, - { note: "c", id: 3, count: 2 }, -]; -items[0]!.note = "added"; -items[0]!.name = "later"; -items[1]!.name = "kept"; -delete items[2]!.note; -items[2]!.note = "again"; -for (const item of items) { - if (item.count > 0) item.name ??= "filled"; - console.log(JSON.stringify(item), Object.keys(item).join(",")); -} -const built: Item = { count: 9, id: 9 }; -for (let i = 0; i < 2; i++) { - if (i === 1) built.note = `n${i}`; - console.log(Object.keys(built).join(",")); -} -console.log(built); -// A write's value runs first, so a key it adds comes before the written -// key. -const nested: Item = { count: 3, id: 4 }; -nested.name = ((nested.note = "first"), "second"); -console.log(Object.keys(nested).join(","), JSON.stringify(nested)); -// A value seen as a JavaScript value enumerates later writes in order. -const seen: Item = { id: 5, count: 4 }; -const view: unknown = seen; -console.log(Object.keys(view as object).join(",")); -seen.note = "n"; -seen.name = "m"; -console.log(Object.keys(view as object).join(","), JSON.stringify(view)); -// The write goes to the object the receiver held before the value ran, -// even when the value assigns the receiver's binding. -let target: Item = { id: 6, count: 5 }; -const before = target; -const after: Item = { count: 6, id: 7 }; -target.name = ((target = after), "moved"); -console.log(Object.keys(before).join(","), Object.keys(after).join(","), JSON.stringify(target)); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index 0cf8bcae5..83c3ede49 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,18 +20,14 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: values of one type are built in two orders, and a spread sets -// an optional key its source lacks, so the values cannot record their own -// orders and the type keeps its declaration order. +// Key order: values of one type are built in two orders and the program +// observes them, so the type keeps its declaration order. interface Point { x: number; y: number; - label?: string; } const point: Point = { y: 1, x: 2 }; -const other: Point = { x: 3, y: 4 }; -const labeled: Point = { ...point, label: "p" }; -console.log(JSON.stringify(point), JSON.stringify(other), JSON.stringify(labeled)); +console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/harness/__snapshots__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt index 8ab286873..66373655f 100644 --- a/tests/harness/__snapshots__/coverage-divergences.txt +++ b/tests/harness/__snapshots__/coverage-divergences.txt @@ -1,7 +1,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts - statements analyzed 20 - compile statically 20 (100%) + statements analyzed 18 + compile statically 18 (100%) in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) fully static — all analyzed statements compile to native code. @@ -14,11 +14,11 @@ scriptc coverage tests/coverage-fixtures/divergences.ts at tests/coverage-fixtures/divergences.ts:20:23 hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class ×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in … - at tests/coverage-fixtures/divergences.ts:31:22 + at tests/coverage-fixtures/divergences.ts:29:22 hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built) ×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation - at tests/coverage-fixtures/divergences.ts:37:70 + at tests/coverage-fixtures/divergences.ts:33:70 hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree ×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally - at tests/coverage-fixtures/divergences.ts:52:20 + at tests/coverage-fixtures/divergences.ts:48:20 hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share \ No newline at end of file diff --git a/tests/harness/corpus-costs/7132-key-order-json.ts.json b/tests/harness/corpus-costs/7132-key-order-json.ts.json deleted file mode 100644 index aa8658007..000000000 --- a/tests/harness/corpus-costs/7132-key-order-json.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json b/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json deleted file mode 100644 index 58d7b29f8..000000000 --- a/tests/harness/corpus-costs/7133-key-order-keys-values-entries.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1500, "san": 2800 } diff --git a/tests/harness/corpus-costs/7134-key-order-for-in.ts.json b/tests/harness/corpus-costs/7134-key-order-for-in.ts.json deleted file mode 100644 index e0b1e4dd1..000000000 --- a/tests/harness/corpus-costs/7134-key-order-for-in.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1600, "san": 2100 } diff --git a/tests/harness/corpus-costs/7135-key-order-inspect.ts.json b/tests/harness/corpus-costs/7135-key-order-inspect.ts.json deleted file mode 100644 index 932ea4809..000000000 --- a/tests/harness/corpus-costs/7135-key-order-inspect.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1600, "san": 2500 } diff --git a/tests/harness/corpus-costs/7136-key-order-copies.ts.json b/tests/harness/corpus-costs/7136-key-order-copies.ts.json deleted file mode 100644 index 9d76a548c..000000000 --- a/tests/harness/corpus-costs/7136-key-order-copies.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1900, "san": 3500 } diff --git a/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json b/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json deleted file mode 100644 index 12680c6af..000000000 --- a/tests/harness/corpus-costs/7137-key-order-dynamic.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1800, "san": 2700 } diff --git a/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json deleted file mode 100644 index 2b1aad28d..000000000 --- a/tests/harness/corpus-costs/7138-key-order-late-writes.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1700, "san": 2800 } From 93d2797bacbe14146ad77ceb9d34c545c25bf8ee Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:22:20 -0500 Subject: [PATCH 063/169] Version charAt loops and compare characters as code units `for (let i = 0; i < s.length; i++) if (s[i] === " ") n++` and the same loop over `s.charAt(i)` got no loop version: `s[i]` lowers to charAt, and the version covered only charCodeAt reads. Each character paid the inline helper's ASCII and bounds tests and kept its opaque fallback. `const c = s[i]; if (c >= "a" && c <= "z") n++` also built the character string and compared strings twice. - The typed-array loop version covers `s.charAt(index)` like `s.charCodeAt(index)`, and a counted loop that reads either at its counter takes an integer counter. In the fast copy, a charAt used as a string is the immortal one-character string for its byte, and the equality and switch rewrites to charCodeAt read the byte. - A comparison of `s.charAt(i)` with a one-unit literal compares the code unit, as the equality does. An index out of range reads "", which is below every one-unit literal; its unit read is NaN, which the unordered predicates count as below. - A const string local initialized from a one-argument charAt and used only in one-unit equalities, comparisons and switches holds the code unit instead (char-unit-locals.ts). --- .../backend/llvm/bytes-loop-versions.test.ts | 16 ++ .../src/backend/llvm/bytes-loop-versions.ts | 20 +- .../src/backend/llvm/emit-statements.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 13 +- .../src/backend/llvm/expr-containers.ts | 42 +++- .../src/backend/llvm/expr-primitives.ts | 45 +++-- .../compiler/src/backend/llvm/string-reads.ts | 23 +++ .../compiler/src/ir/char-unit-locals.test.ts | 98 ++++++++++ packages/compiler/src/ir/char-unit-locals.ts | 177 +++++++++++++++++ packages/compiler/src/ir/integer-loops.ts | 14 +- .../test/string-char-loop-emission.test.ts | 71 +++++++ .../7303-string-char-loop-versions.ts.json | 6 + .../corpus/7303-string-char-loop-versions.ts | 184 ++++++++++++++++++ .../7303-string-char-loop-versions.ts.json | 1 + 14 files changed, 681 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/src/ir/char-unit-locals.test.ts create mode 100644 packages/compiler/src/ir/char-unit-locals.ts create mode 100644 packages/compiler/test/string-char-loop-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json create mode 100644 tests/corpus/7303-string-char-loop-versions.ts create mode 100644 tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index 805715386..cee91c66d 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -536,6 +536,22 @@ test("charCodeAt reads of a string binding version with the typed-array reads", expect(sliced.bounds.has(read)).toBe(false); }); +test("charAt reads of a string binding version as one-unit reads", () => { + const read: IrExpr = { + kind: "strIntrinsic", + method: "charAt", + receiver: ref("text", STR), + args: [bin("-", ref("i"), num(1))], + type: STR, + loc, + }; + const loop = counted([{ kind: "exprStmt", expr: read, loc }]); + const version = match(textKernel([loop]), loop)!; + expect(version.bounds.has(read)).toBe(true); + expect(version.receivers.map((r) => r.string)).toEqual([true]); + expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([[0, -1, -1]]); +}); + test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => { const loop = counted([set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i")))]); const kernel = textKernel([loop]); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 35937328f..704b6723a 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -65,7 +65,7 @@ export interface BytesLoopVersion { * thread-owned (no shared storage). */ owned: ReadonlySet; /** Root receiver bindings, read once before the loop; `string` marks a - * string whose `charCodeAt` reads the version covers. */ + * string whose `charCodeAt` and `charAt` reads the version covers. */ receivers: { ref: VarRef; global: boolean; string: boolean }[]; /** Receivers the guard requires to have no shared storage. */ ownedReceivers: number[]; @@ -132,11 +132,13 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * such as `base` for `const base = r * cols`, must also be an exact * integer between -2^32 and 2^32, which the guard tests. * - * `s.charCodeAt(index)` of a string binding qualifies the same way. A - * string is immutable: its bytes, its length and the capacity-word bit that - * proves it ASCII (string-reads.ts) are fixed when it is built. The guard - * also requires that bit, so each UTF-16 unit is one byte and the length - * is the byte length, and the fast copy reads each unit with one byte load. + * `s.charCodeAt(index)` and `s.charAt(index)` (so `s[index]`) of a string + * binding qualify the same way. A string is immutable: its bytes, its + * length and the capacity-word bit that proves it ASCII (string-reads.ts) + * are fixed when it is built. The guard also requires that bit, so each + * UTF-16 unit is one byte and the length is the byte length. The fast copy + * reads each unit with one byte load, and each character as the immortal + * one-character string for its byte. * * In worker programs, every access of a view that may be shared tests its * shared storage. A view's storage is also fixed at construction, so the @@ -507,7 +509,11 @@ export function matchBytesLoopVersion( const numeric = byteNumberAccess(e); if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width); else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1); - } else if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0]) + } else if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] + ) access(e, e.receiver, e.args[0], 1); return everyExprChild(e, visitExpr, visitStmt); }; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 913c6e2c9..b530f794d 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -32,7 +32,7 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js"; import { boxAccess, boxNewCall, elemAccess } from "./shapes.js"; import { dynHoleRead } from "./loop-array-borrows.js"; import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; -import { emitDiscardedSplice } from "./expr-containers.js"; +import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js"; import { InternalCompilerError } from "../../errors.js"; import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; @@ -2531,7 +2531,7 @@ function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "sw return null; return { ...s, - disc: { ...disc, method: "charCodeAt", type: F64 }, + disc: charAtUnitRead(disc), cases: s.cases.map((c) => c.test?.kind === "strLit" ? { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ef512dddd..7d4ed09f3 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -90,6 +90,7 @@ import { foldConstantGlobals } from "../../ir/constant-globals.js"; import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js"; import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js"; import { hoistFinalFieldReads } from "../../ir/field-values.js"; +import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js"; import { findInt32Abis, INT32_ABI_SUFFIX, @@ -497,7 +498,11 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) foldConstantGlobals( copyStableCaptures( resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + ), + ), ), ), ), @@ -531,7 +536,11 @@ export function emitLlvmModuleSource( foldConstantGlobals( copyStableCaptures( resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + ), + ), ), ), ), diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 74dea62c4..2f4cd98e9 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -2,7 +2,7 @@ import { arraySearchHelperName, arraySearchKind } from "./array-search.js"; import { InternalCompilerError } from "../../errors.js"; import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js"; -import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; import { mangleResolveThunk } from "../mangle.js"; import { arrOps, @@ -51,6 +51,7 @@ import { STRING_CHAR_AT_INT_MARKER, STRING_CHAR_CODE_AT_DECL, STRING_CHAR_CODE_AT_INT_MARKER, + emitAsciiChar, emitAsciiCharCode, STRING_UTF16_LEN_DECL, } from "./string-reads.js"; @@ -285,6 +286,36 @@ export function emitArrayCopyLoop( host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`); } +/** The `charCodeAt` reads that stand for `charAt` reads the emitter + * rewrote, such as one-unit tests and switches: a loop version's proof + * for the charAt covers its read. */ +const unitReadOrigins = new WeakMap(); + +/** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a + * code unit. */ +export function charAtUnitRead( + charAt: IrExpr & { kind: "strIntrinsic" }, +): IrExpr & { kind: "strIntrinsic" } { + const read: IrExpr & { kind: "strIntrinsic" } = { + kind: "strIntrinsic", + method: "charCodeAt", + receiver: charAt.receiver, + args: charAt.args, + type: F64, + loc: charAt.loc, + }; + unitReadOrigins.set(read, charAt); + return read; +} + +/** An enclosing loop version proved the string of `e` ASCII and its index + * below the length (bytes-loop-versions.ts). */ +function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean { + if (host.bytesBounds.has(e)) return true; + const origin = unitReadOrigins.get(e); + return origin !== undefined && host.bytesBounds.has(origin); +} + export function emitStrIntrinsic( host: LlvmEmitterContext, e: IrExpr & { kind: "strIntrinsic" }, @@ -331,9 +362,7 @@ export function emitStrIntrinsic( const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { const wide = widenInteger(host, index); - // An enclosing loop version proved the string ASCII and the index - // below its length (bytes-loop-versions.ts). - if (host.bytesBounds.has(e)) + if (provenStringRead(host, e)) return { name: emitAsciiCharCode(host, r.name, wide), type: e.type }; host.declare(STRING_CHAR_CODE_AT_INT_MARKER); B.line(`${t} = call double @sc_str_char_code_at_int(ptr ${r.name}, i64 ${wide})`); @@ -348,8 +377,11 @@ export function emitStrIntrinsic( const t = B.tmp(); const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null; if (index) { - host.declare(STRING_CHAR_AT_INT_MARKER); const wide = widenInteger(host, index); + // The table entry is immortal: nothing owns it. + if (provenStringRead(host, e)) + return host.markImmortal({ name: emitAsciiChar(host, r.name, wide), type: e.type }); + host.declare(STRING_CHAR_AT_INT_MARKER); B.line(`${t} = call ptr @sc_str_char_at_int(ptr ${r.name}, i64 ${wide})`); } else { const at = args[0]?.name ?? f64Lit(0); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 02ffd3c82..72392956a 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -1,7 +1,7 @@ /* Focused LLVM expression emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js"; -import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; +import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js"; import { mangleRecordClone, mangleRecordNew } from "../mangle.js"; import { arrNewCall, elemAccess } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -25,7 +25,7 @@ import { withinInt32, withinUint32, } from "../../ir/integer-ranges.js"; -import { emitArrayValues } from "./expr-containers.js"; +import { charAtUnitRead, emitArrayValues } from "./expr-containers.js"; import { constantDivisor, emitCheckedRemainder, @@ -567,11 +567,12 @@ export function regexLiteralTemplate( return `@${re.sym}`; } -/** A string equality between `s.charAt(i)` and a literal of exactly one - * UTF-16 code unit: the charAt call and the unit. */ +/** A string equality or comparison between `s.charAt(i)` and a literal of + * exactly one UTF-16 code unit: the charAt call, the unit, and whether the + * charAt is the left operand. */ function charAtUnitTest( - e: ExprOf<"strEq">, -): { charAt: ExprOf<"strIntrinsic">; code: number } | null { + e: ExprOf<"strEq" | "strCmp">, +): { charAt: ExprOf<"strIntrinsic">; code: number; left: boolean } | null { let text: string; let other: IrExpr; if (e.right.kind === "strLit") { @@ -586,7 +587,17 @@ function charAtUnitTest( if (text.length !== 1) return null; if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1) return null; - return { charAt: other, code: text.charCodeAt(0) }; + return { charAt: other, code: text.charCodeAt(0), left: other === e.left }; +} + +/** The fcmp predicate of `s.charAt(i) op "c"` on the code unit (or of + * `"c" op s.charAt(i)` when `charAtLeft` is false). A one-unit string + * compares as its unit; an index out of range reads "", which is below + * every one-unit literal, and its unit read is NaN, which the unordered + * predicates count as below. */ +function charAtUnitPredicate(op: IrStrCmpOp, charAtLeft: boolean): string { + const flipped = charAtLeft ? op : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[op]; + return { "<": "ult", "<=": "ule", ">": "ogt", ">=": "oge" }[flipped]; } export function emitStringExpr( @@ -637,14 +648,7 @@ export function emitStringExpr( // equals no unit, as "" equals no one-unit literal. const unit = charAtUnitTest(e); if (unit !== null) { - const code = host.emitExpr({ - kind: "strIntrinsic", - method: "charCodeAt", - receiver: unit.charAt.receiver, - args: unit.charAt.args, - type: F64, - loc: unit.charAt.loc, - }); + const code = host.emitExpr(charAtUnitRead(unit.charAt)); const t = B.tmp(); B.line( `${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`, @@ -674,6 +678,17 @@ export function emitStringExpr( return { name: t, type: e.type }; } case "strCmp": { + // `s.charAt(i) < "c"` (and `s[i]`) for a one-unit literal compares + // the code unit, as the equality above does. + const unit = charAtUnitTest(e); + if (unit !== null) { + const code = host.emitExpr(charAtUnitRead(unit.charAt)); + const t = B.tmp(); + B.line( + `${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`, + ); + return { name: t, type: e.type }; + } const [l, r] = emitStringInputs(host, [e.left, e.right]); const fn = "scr_str_cmp_u16"; host.declare(`declare i32 @${fn}(ptr, ptr)`); diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index c5841006c..441c925f3 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -75,6 +75,29 @@ export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: st return code; } +/** The one-character string of unit `index` (an i64) of a proven-ASCII + * string `s` whose length covers it: the immortal `@sc_ascii_chars` entry + * for its byte, as the inline charAt helpers answer. */ +export function emitAsciiChar(host: LlvmEmitterContext, s: string, index: string): string { + // The table is defined with sc_str_char_at. + host.declare(STRING_CHAR_AT_DECL); + const B = host.B; + const sz = host.sizeType; + const data = B.tmp(), + pointer = B.tmp(), + byte = B.tmp(), + code = B.tmp(), + entry = B.tmp(); + B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${s}, i32 1`); + B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, i64 ${index}`); + B.line(`${byte} = load i8, ptr ${pointer}`); + B.line(`${code} = zext i8 ${byte} to i64`); + B.line( + `${entry} = getelementptr inbounds [128 x { ${sz}, ${sz}, ${sz}, [2 x i8] }], ptr @sc_ascii_chars, i64 0, i64 ${code}`, + ); + return entry; +} + /** Load the proven-ASCII flag of `%s` into `%ascii`. */ function asciiTest(): string[] { return [ diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts new file mode 100644 index 000000000..ad16ecebd --- /dev/null +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -0,0 +1,98 @@ +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 "../index.js"; +import { holdCharLocalsAsUnits } from "./char-unit-locals.js"; +import type { IrFunction, IrModule } from "./ir.js"; +import { everyStmtList } from "./traverse.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-char-units-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + 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")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const fn = (mod: IrModule, name: string): IrFunction => mod.functions.find((f) => f.name === name)!; + +/** The kinds of the function's string tests and string intrinsics. */ +function reads(f: IrFunction): string[] { + const out: string[] = []; + everyStmtList(f.body, { + stmt: () => true, + expr: (e) => { + if (e.kind === "strEq" || e.kind === "strCmp") out.push(e.kind); + if (e.kind === "strIntrinsic") out.push(e.method); + return true; + }, + }); + return out; +} + +test("a character local used only in one-unit tests holds its code unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function classify(s: string, i: number): number { + const c = s.charAt(i); + let n = 0; + if (c === " ") n += 1; + if (c !== "\\t") n += 2; + if ("a" <= c && c <= "z") n += 4; + if (c < "0" || c > "9") n += 8; + switch (c) { + case "5": + n += 16; + break; + default: + n += 32; + } + return n; +} +console.log(classify("a b", 1), classify("x", 0), classify("7", 0), classify("5", 0), classify("", 3)); +`), + ); + const classify = fn(mod, "classify"); + expect(reads(classify)).toEqual(["charCodeAt"]); + expect(classify.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); +}); + +test("a character local with any other use stays a string", async () => { + const before = await lower(` +function escapes(s: string, i: number): string { + const c = s.charAt(i); + return c === " " ? "space" : c; +} +function rebound(s: string, i: number): boolean { + let c = s.charAt(i); + if (c === " ") c = s.charAt(i + 1); + return c === "x"; +} +function wide(s: string, i: number): boolean { + const c = s.charAt(i); + return c === "ab" || c < "b"; +} +function against(s: string, t: string, i: number): boolean { + const c = s.charAt(i); + return c < t; +} +function captured(s: string, i: number): () => boolean { + const c = s.charAt(i); + return () => c === "y"; +} +console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b", 0), captured("y", 0)()); +`); + const after = holdCharLocalsAsUnits(before); + for (const name of ["escapes", "rebound", "wide", "against", "captured"]) { + expect(fn(before, name)).toBeDefined(); + expect(fn(after, name)).toBe(fn(before, name)); + } +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts new file mode 100644 index 000000000..3a5c7cd42 --- /dev/null +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -0,0 +1,177 @@ +import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { mapExprChildren, mapStmtChildren } from "./traverse.js"; + +type CharAt = IrExpr & { kind: "strIntrinsic" }; +type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; + +/** Character locals held as their code unit. + * + * `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as + * `c === " "`, `c >= "a" && c <= "z"` or `switch (c) { case "x": ... }` + * against literals of one UTF-16 code unit builds a one-character string + * and compares strings, though every answer follows from the code unit. + * Such a local holds `s.charCodeAt(i)` instead, and each test compares + * numbers: + * - a one-unit string compares as its unit; + * - charAt reads "" for an index out of range, and charCodeAt reads NaN. + * "" equals no one-unit literal and is below each, while NaN equals no + * unit and fails every ordered comparison, so `c < "x"` becomes + * `!(c >= 120)`. + * + * Only immutable unboxed string locals qualify, with one declaration whose + * initializer is a one-argument charAt, and with no other mention of the + * local. */ +export function holdCharLocalsAsUnits(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn) => { + const next = rewriteFunction(fn); + if (next !== fn) changed = true; + return next; + }); + return changed ? { ...mod, functions } : mod; +} + +function rewriteFunction(fn: IrFunction): IrFunction { + const candidates = new Set( + fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id), + ); + if (candidates.size === 0) return fn; + // Every mention of a candidate, in any field, against the mentions the + // rewrite understands. + const mentions = new Map(); + const understood = new Map(); + const declarations = new Map(); + const note = (map: Map, id: string): void => { + map.set(id, (map.get(id) ?? 0) + 1); + }; + const count = (value: unknown): void => { + if (typeof value === "string") { + if (candidates.has(value)) note(mentions, value); + } else if (Array.isArray(value)) { + for (const item of value) count(item); + } else if (value !== null && typeof value === "object") { + const node = value as Record; + const id = understoodMention(node); + if (id !== null && candidates.has(id)) { + note(understood, id); + if (node.kind === "varDecl") note(declarations, id); + } + for (const key in node) if (key !== "loc") count(node[key]); + } + }; + count(fn.body); + const held = new Set(); + for (const id of candidates) + if ( + declarations.get(id) === 1 && + (mentions.get(id) ?? 0) > 0 && + mentions.get(id) === understood.get(id) + ) + held.add(id); + if (held.size === 0) return fn; + + const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({ + kind: "varRef", + localId: id, + type: F64, + loc, + }); + const rewriteExpr = (e: IrExpr): IrExpr => { + if (e.kind === "strEq" || e.kind === "strCmp") { + const use = unitUse(e); + if (use && held.has(use.id)) { + const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc }; + const c = unitRef(use.id, e.loc); + if (e.kind === "strEq") + return { + kind: "bin", + op: e.negated ? "!==" : "===", + left: c, + right: code, + type: BOOL, + loc: e.loc, + }; + // The comparison with the local on the left. + const op = use.left + ? e.op + : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op]; + if (op === ">" || op === ">=") + return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc }; + const negation: IrExpr = { + kind: "bin", + op: op === "<" ? ">=" : ">", + left: c, + right: code, + type: BOOL, + loc: e.loc, + }; + return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; + } + } + return mapExprChildren(e, rewriteExpr, rewriteStmt); + }; + const rewriteStmt = (s: IrStmt): IrStmt => { + if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") + return { + ...s, + init: { + ...s.init, + method: "charCodeAt", + receiver: rewriteExpr(s.init.receiver), + args: s.init.args.map(rewriteExpr), + type: F64, + }, + }; + if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) + return { + ...s, + disc: unitRef(s.disc.localId, s.disc.loc), + cases: s.cases.map((c) => ({ + ...c, + test: + c.test?.kind === "strLit" + ? { kind: "numLit", value: c.test.value.charCodeAt(0), type: F64, loc: c.test.loc } + : c.test, + body: c.body.map(rewriteStmt), + })), + }; + return mapStmtChildren(s, rewriteExpr, rewriteStmt); + }; + return { + ...fn, + body: fn.body.map(rewriteStmt), + locals: fn.locals.map((l) => (held.has(l.id) ? { ...l, type: F64 } : l)), + }; +} + +/** The local a node mentions in a form the rewrite handles: the + * declaration from a one-argument charAt, a one-unit test, or a switch + * over one-unit cases. */ +function understoodMention(node: Record): string | null { + const n = node as unknown as IrExpr | IrStmt; + if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null; + if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null; + if (n.kind === "switch") { + if (n.disc.kind !== "varRef") return null; + if (!n.cases.some((c) => c.test !== null)) return null; + const units = n.cases.every( + (c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1), + ); + return units ? n.disc.localId : null; + } + return null; +} + +function isCharAt(e: IrExpr): e is CharAt { + return e.kind === "strIntrinsic" && e.method === "charAt" && e.args.length === 1; +} + +/** A test between a local and a literal of one code unit. */ +function unitUse(e: UnitUse): { id: string; code: number; left: boolean } | null { + const literalRight = e.right.kind === "strLit"; + const literal = literalRight ? e.right : e.left; + const other = literalRight ? e.left : e.right; + if (literal.kind !== "strLit" || literal.value.length !== 1 || other.kind !== "varRef") + return null; + return { id: other.localId, code: literal.value.charCodeAt(0), left: literalRight }; +} diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index c451c0852..f54f6618b 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -333,7 +333,12 @@ export function matchIntegerCountedForLoop( ) useful = true; // A string read at the counter indexes with the integer directly. - if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && counter(e.args[0])) + if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] && + counter(e.args[0]) + ) useful = true; // So does an array element read; only a loop that needs no versioned // copy takes the integer counter for it. @@ -456,7 +461,12 @@ export function matchIntegerPairedForLoop( (isCounter(e.left) || isCounter(e.right))); const expr = (e: IrExpr): boolean => { if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; - if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) + if ( + e.kind === "strIntrinsic" && + (e.method === "charCodeAt" || e.method === "charAt") && + e.args[0] && + isCounter(e.args[0]) + ) useful = true; if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) useful = true; diff --git a/packages/compiler/test/string-char-loop-emission.test.ts b/packages/compiler/test/string-char-loop-emission.test.ts new file mode 100644 index 000000000..6f5756ce8 --- /dev/null +++ b/packages/compiler/test/string-char-loop-emission.test.ts @@ -0,0 +1,71 @@ +import { execFileSync } from "node:child_process"; +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"; + +test("character reads of ASCII strings in versioned loops read bytes", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-char-loops-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + ` +function spaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s[i] === " ") n++; + return n; +} +function letters(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charAt(i); + if (c >= "a" && c <= "z") n++; + } + return n; +} +function chars(s: string): string[] { + const out: string[] = []; + for (let i = 0; i < s.length; i++) out.push(s.charAt(i)); + return out; +} +console.log(spaces("a b c"), letters("abC1z"), chars("xyz").join("-"), chars("é!").join("-")); +`, + ); + const outPath = join(dir, "main.ir.json"); + const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!lowered.ok) + throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + ll.match( + new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`), + )![0]; + // The fast copy follows the checked one. + const fast = (fn: string): string => fn.slice(fn.lastIndexOf("bytes.version.fast")); + + // `s[i] === " "` compares the byte; the checked copy keeps the helper. + const spaces = body("spaces"); + expect(fast(spaces)).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + expect(fast(spaces)).not.toContain("@sc_str_char_code_at_int"); + expect(spaces).toContain("@sc_str_char_code_at_int"); + // A character local compared with one-unit literals holds its unit. + const letters = body("letters"); + expect(letters).not.toContain("@sc_str_cmp_u16"); + expect(fast(letters)).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/); + // A character used as a string is its immortal table entry. + const chars = body("chars"); + expect(fast(chars)).toContain("@sc_ascii_chars"); + expect(fast(chars)).not.toContain("@sc_str_char_at_int"); + expect(chars).toContain("@sc_str_char_at_int"); + + const exe = join(dir, "main"); + const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + expect(execFileSync(exe, { encoding: "utf8" })).toBe("2 3 x-y-z é-!\n"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json new file mode 100644 index 000000000..58242e583 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7303-string-char-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7303-string-char-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7303-string-char-loop-versions.ts b/tests/corpus/7303-string-char-loop-versions.ts new file mode 100644 index 000000000..f6c21fc59 --- /dev/null +++ b/tests/corpus/7303-string-char-loop-versions.ts @@ -0,0 +1,184 @@ +// s[i] and s.charAt(i) in counted for loops: equality and switch tests with +// one-unit literals, character locals compared with ranges of one-unit +// literals on either side, characters used as strings (pushed, concatenated, +// map keys, compared with each other), reads past the end, fractional, +// negative and -0 indices, ASCII, Latin-1, BMP, surrogate pairs and lone +// surrogates, empty strings, a character local with other uses, a local +// compared with a longer literal, a string rebound in the loop and a +// module-level loop. + +function spacesIndex(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s[i] === " ") n++; + return n; +} + +function nonSpaces(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) if (s.charAt(i) !== " ") n++; + return n; +} + +function kinds(s: string): string { + let letters = 0, + digits = 0, + upper = 0, + other = 0; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c >= "a" && c <= "z") letters++; + else if ("0" <= c && "9" >= c) digits++; + else if (c > "@" && c < "[") upper++; + else other++; + } + return `${letters}/${digits}/${upper}/${other}`; +} + +function punctuation(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charAt(i); + switch (c) { + case ".": + case ",": + n += 1; + break; + case "!": + n += 10; + break; + case "é": + n += 100; + break; + default: + if (c === "?") n += 1000; + } + } + return n; +} + +function switchIndex(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + switch (s[i]) { + case "a": + n++; + break; + case "b": + n += 2; + break; + } + } + return n; +} + +function pastEnd(s: string): string { + let below = 0, + atMost = 0, + above = 0, + atLeast = 0; + for (let i = 0; i <= s.length + 1; i++) { + const c = s.charAt(i); + if (c < "a") below++; + if (c <= "z") atMost++; + if (c > " ") above++; + if (c >= "\0") atLeast++; + } + return `${below} ${atMost} ${above} ${atLeast}`; +} + +function oddIndices(s: string): string { + const out: string[] = []; + for (let i = -2; i < 3; i++) out.push(JSON.stringify(s.charAt(i + 0.5))); + out.push(JSON.stringify(s.charAt(-0)), JSON.stringify(s.charAt(NaN))); + const c = s.charAt(-1); + out.push(String(c < "a"), String(c >= "\0")); + return out.join(","); +} + +function chars(s: string): string { + const out: string[] = []; + for (let i = 0; i < s.length; i++) out.push(s.charAt(i)); + let reversed = ""; + for (let i = s.length - 1; i >= 0; i--) reversed += s[i]; + return `${out.join("|")} ${reversed}`; +} + +function counts(s: string): string { + const seen = new Map(); + for (let i = 0; i < s.length; i++) { + const c = s[i]; + seen.set(c, (seen.get(c) ?? 0) + 1); + } + return [...seen].map(([c, n]) => `${c}${n}`).join(""); +} + +function sameAt(a: string, b: string): number { + let n = 0; + const end = Math.min(a.length, b.length); + for (let i = 0; i < end; i++) if (a[i] === b[i]) n++; + return n; +} + +function words(s: string): string { + let n = 0; + let last = ""; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === " ") n++; + last = c; + } + return `${n}:${last}`; +} + +function longer(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charAt(i); + if (c === "ab" || c < "b") n++; + } + return n; +} + +function rebound(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === "x") n++; + if (i === 1) s = s + "xx"; + } + return n; +} + +const samples = [ + "the quick brown fox, jumps over the lazy dog!", + "", + " ", + "abc123 DEF? xyz", + "café, au lait.", + "日本語のテキスト と 空白", + "emoji 😀 and 🎉 here!", + "lone \uD800 surrogate \uDFFF end", + "aabbab ba", +]; +for (const s of samples) { + console.log( + JSON.stringify(s), + spacesIndex(s), + nonSpaces(s), + kinds(s), + punctuation(s), + switchIndex(s), + pastEnd(s), + oddIndices(s), + ); + console.log(" ", chars(s), counts(s), words(s), longer(s), rebound(s)); +} + +let built = ""; +for (let k = 0; k < 40; k++) built += String.fromCharCode(97 + (k % 26)) + (k % 4 === 0 ? " " : ""); +console.log("built", spacesIndex(built), kinds(built), sameAt(built, built.slice(3)), counts(built)); +console.log("pairs", sameAt("hello world", "help, world!"), sameAt("abc", ""), sameAt("é", "e")); + +const text = "module level: scan, with commas, and spaces"; +let commas = 0; +for (let i = 0; i < text.length; i++) if (text[i] === ",") commas++; +console.log("module", commas); diff --git a/tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json b/tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json new file mode 100644 index 000000000..0c31a05f7 --- /dev/null +++ b/tests/harness/corpus-costs/7303-string-char-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4000 } From d4a7b82dd80cff07a88eb3b2cf7cba38264eeb18 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:22:55 -0500 Subject: [PATCH 064/169] Free arrays whose elements need no release without a teardown When the last reference to an array dropped, the runtime ran the generic teardown: scr_rc_destroy's depth bookkeeping, an indirect call to the array destroy, and a loop over the element slots. Arrays of numbers, booleans and compact ProgramLifetime references release nothing, and neither do arrays of other ProgramLifetime instances, whose release does nothing. - scr_arr_release frees an acyclic array without an ext directly when its elements are numbers, booleans or compact references, or its descriptor has SCR_ARR_OPS_RELEASE_NOTHING. Every other array keeps the full teardown. - The backend sets SCR_ARR_OPS_RELEASE_NOTHING in the descriptor's flags word on 64-bit targets for ProgramLifetime element types that are not compact, plain or as the pointer arm of a nullable union, in programs without lifetime regions. Registration copies it into a table indexed by the descriptor's registry id before the id is published. Descriptors without the flag read 0 and keep the full teardown. Corpus 7401 drops arrays of ProgramLifetime instances, numbers and booleans through every array operation, with holes and far indices, next to arrays of ordinary objects, and matches Node. --- .../compiler/src/backend/llvm/classes.test.ts | 37 +++++++ packages/compiler/src/backend/llvm/emitter.ts | 5 + packages/compiler/src/backend/llvm/shapes.ts | 19 +++- ...01-program-lifetime-array-teardown.ts.json | 6 ++ packages/runtime/src/scr_array.c | 42 ++++++-- packages/runtime/src/scr_array.h | 19 ++++ packages/runtime/test/test_array.c | 39 +++++++ .../7401-program-lifetime-array-teardown.ts | 102 ++++++++++++++++++ ...01-program-lifetime-array-teardown.ts.json | 1 + 9 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json create mode 100644 tests/corpus/7401-program-lifetime-array-teardown.ts create mode 100644 tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index f921900a3..bee954afb 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -168,6 +168,43 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts", expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp"); }); +test("arrays of ProgramLifetime instances tell the runtime their teardown releases nothing", () => { + const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } }; + const owners = { kind: "array" as const, elem: { kind: "object" as const, className: "Owner" } }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { + name: "main", + params: [], + returnType: VOID, + locals: [], + body: [ + { kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: kids, loc }, loc }, + { kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: owners, loc }, loc }, + ], + loc, + }, + ], + classes: [ + { name: "Node", fields: [], methods: [], loc }, + { name: "Owner", fields: [{ name: "nodes", type: kids }], methods: [], loc }, + ], + contracts: { programLifetime: ["Node"] }, + }; + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const ops = (cls: string): string => + new RegExp(`^@sc_arrops\\.[^\\n]*${mangleClassRelease(cls)}[^\\n]*$`, "m").exec(llvm)?.[0] ?? + ""; + // SCR_ARR_OPS_RELEASE_NOTHING for Node; Owner's elements are released. + expect(ops("Node")).toMatch( + /= internal global \{ ptr, ptr, ptr, i32, i32 \} .*, i32 0, i32 2 \}$/, + ); + expect(ops("Owner")).toMatch(/= internal global %ScrArrOps .*, i32 0 \}$/); +}); + test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => { const mapper = { kind: "object" as const, className: "Mapper" }; const mod: IrModule = { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ef512dddd..8542963e0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1591,6 +1591,11 @@ export class LlEmitter { dynPromiseAdapter: (type) => this.dynPromiseAdapter(type), isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error", classRootOf: (name) => this.classMeta.get(name)?.root.def.name, + releasesNothing: (t) => { + if (t.kind !== "object" || (this.shapeHost.regions ?? null) !== null) return false; + const meta = this.classMeta.get(t.className); + return meta !== undefined && programLifetimeClass(this.shapeHost, meta); + }, classSubtypes: (name) => { const target = this.classMetaOf(name); const intervals = classMembershipIntervals(this.classMeta, target.def.name); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 8ae24375e..d13f80a78 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -78,6 +78,10 @@ export interface ShapeHost { /** The hierarchy root of a class (a program class or a runtime base), * or undefined for classes outside the class graph. */ classRootOf?(className: string): string | undefined; + /** Whether releasing a value of this type does nothing: instances of a + * program-lifetime class without lifetime regions (classes.ts + * programLifetimeClass). */ + releasesNothing?(t: IrType): boolean; /** Whether arrays of `elem` hold 32-bit references to compact class * instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */ compactArrayElem?(elem: IrType): boolean; @@ -1117,7 +1121,9 @@ export function arrNewCall( * writable: the runtime stores its registry id on the first array. Its tail * padding is the runtime's flags word (0); arrays of compact class * references (ShapeHost.compactArrayElem) spell the word out as - * SCR_ARR_OPS_COMPACT. */ + * SCR_ARR_OPS_COMPACT, and on 64-bit targets arrays whose element release + * does nothing (ShapeHost.releasesNothing) as SCR_ARR_OPS_RELEASE_NOTHING, + * so that destroying them only frees their storage. */ export function arrOps(host: ShapeHost, elem: IrType): string { const v = vAdapters(host, elem); const trace = traceArg(host, elem); @@ -1129,14 +1135,23 @@ export function arrOps(host: ShapeHost, elem: IrType): string { ); return `${name}.c`; } + const nullable = host.nullableUnions.of(elem); + const payload = nullable?.repr === "single" ? nullable.arm : elem; + if (host.sizeType === "i64" && host.releasesNothing?.(payload) === true) { + host.declare( + `${name}.n = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_RELEASE_NOTHING} }`, + ); + return `${name}.n`; + } host.declare( `${name} = internal global %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0 }`, ); return name; } -/** scr_array.h SCR_ARR_OPS_COMPACT. */ +/** scr_array.h SCR_ARR_OPS_COMPACT and SCR_ARR_OPS_RELEASE_NOTHING. */ const SCR_ARR_OPS_COMPACT = 1; +const SCR_ARR_OPS_RELEASE_NOTHING = 2; /* ── capture boxes ────────────────────────────────────────────────────── */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json new file mode 100644 index 000000000..4341cdbdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7401-program-lifetime-array-teardown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7401-program-lifetime-array-teardown.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 3d6b918e9..f3e216a36 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -108,6 +108,7 @@ ScrArrExt *scr_arr_ext(ScrArr *a) { /* Registry of element ops by id (scr_arr_ops); slot 0 is no ops. */ const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW] = {0}; +uint8_t scr_arr_ops_release_nothing[SCR_ARR_OPS_OVERFLOW + 1] = {0}; static uint32_t scr_arr_ops_count = 1; /* Assign `ops` its id on its first array; any thread may get there first, @@ -119,6 +120,9 @@ uint32_t scr_arr_ops_register(ScrArrOps *ops) { if (id) return id; uint32_t fresh = __atomic_fetch_add(&scr_arr_ops_count, 1, __ATOMIC_RELAXED); if (fresh < SCR_ARR_OPS_OVERFLOW) { + /* Before the id is published: an array that carries the id can be + * released on any thread. */ + scr_arr_ops_release_nothing[fresh] = scr_arr_ops_release_nothing_flag(ops); __atomic_store_n(&scr_arr_ops_table[fresh], ops, __ATOMIC_RELEASE); } else { fresh = SCR_ARR_OPS_OVERFLOW; @@ -1530,6 +1534,16 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true); } +/* Frees an acyclic array whose elements are already released. */ +static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) { + scr_arr_free_parts(a); +#ifdef SCR_RC_AUDIT + scr_live_arrays--; +#endif + scr_weak_dispose_inline(a); + scr_mem_free(a); +} + static void scr_arr_destroy(void *object) { ScrArr *a = object; if (scr_arr_ext_of(a)) { @@ -1597,21 +1611,29 @@ static void scr_arr_destroy(void *object) { } } } - if (a->cyc) { - scr_arr_gc_free(a); - } else { - scr_arr_free_parts(a); -#ifdef SCR_RC_AUDIT - scr_live_arrays--; -#endif - scr_weak_dispose_inline(a); - scr_mem_free(a); - } + if (a->cyc) scr_arr_gc_free(a); + else scr_arr_free(a); +} + +/* Whether destroying `a`, an acyclic array, releases nothing: it has no ext + * and its elements are numbers, booleans or compact references, or + * references whose release does nothing (SCR_ARR_OPS_RELEASE_NOTHING). */ +static inline bool scr_arr_releases_nothing(const ScrArr *a) { + if (a->spare & SCR_ARR_HAS_EXT) return false; + if (!scr_elem_is_ref(a->elem)) return true; + return a->elem == SCR_ELEM_REF && + scr_arr_ops_release_nothing[(uint32_t)a->spare >> SCR_ARR_OPS_SHIFT]; } void scr_arr_release(ScrArr *a) { if (!a || a->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--a->rc == 0) { + /* Nothing to tear down: free the storage without scr_rc_destroy's + * queue (no release can recurse) or the element loop. */ + if (!a->cyc && scr_arr_releases_nothing(a)) { + scr_arr_free(a); + return; + } if (a->cyc) scr_cyc_dead(a); scr_rc_destroy(a, scr_arr_destroy); } else if (a->cyc) { diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 689eb63ee..e13acb233 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -89,6 +89,22 @@ static inline bool scr_arr_ops_compact(const ScrArrOps *ops) { #endif } +/* `release` does nothing: the element type's instances live until the + * process exits (llvm/shapes.ts arrOps). Destroying such an array frees only + * its storage. */ +#define SCR_ARR_OPS_RELEASE_NOTHING 2u + +/* Whether `ops` has SCR_ARR_OPS_RELEASE_NOTHING. 32-bit descriptors end + * before the flags word, so they never have it. */ +static inline bool scr_arr_ops_release_nothing_flag(const ScrArrOps *ops) { +#if UINTPTR_MAX > 0xffffffffu + return (ops->flags & SCR_ARR_OPS_RELEASE_NOTHING) != 0; +#else + (void)ops; + return false; +#endif +} + typedef struct ScrArr { size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */ /* Length and dense capacity are at most SCR_ARR_MAX_LENGTH (2^32 - 1); @@ -216,6 +232,9 @@ static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) { #define SCR_ARR_OPS_SHIFT 5 #define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT) extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW]; +/* By registry id: 1 when the descriptor has SCR_ARR_OPS_RELEASE_NOTHING. + * Written before the id is published; the overflow id stays 0. */ +extern uint8_t scr_arr_ops_release_nothing[SCR_ARR_OPS_OVERFLOW + 1]; /* Assigns the id on a descriptor's first array; out of line, so the id * load in scr_arr_ops_id stays the only cost on later arrays. */ __attribute__((cold, noinline)) uint32_t scr_arr_ops_register(ScrArrOps *ops); diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 6e4ead89a..769b8ef3a 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -446,6 +446,44 @@ static void test_ref_elements(void) { #endif } +/* Elements whose release does nothing (SCR_ARR_OPS_RELEASE_NOTHING): the + * last release of an array frees its storage without calling the element + * release. An array with out-of-line storage keeps the full teardown. */ +static int nothing_releases = 0; +static void *nothing_retain(void *p) { return p; } +static void nothing_release(void *p) { + (void)p; + nothing_releases++; +} +static ScrArrOps nothing_ops = { ¬hing_retain, ¬hing_release, NULL, 0, + SCR_ARR_OPS_RELEASE_NOTHING }; + +static void test_release_nothing_ops(void) { +#ifdef SCR_RC_AUDIT + long arrays0 = scr_arr_live_count(); +#endif + static MockRec kept[3]; + ScrArr *a = scr_arr_new_ops(¬hing_ops, 0); + for (int i = 0; i < 3; i++) scr_arr_push_ref(a, &kept[i]); + ScrArr *shared = scr_arr_retain(a); + scr_arr_release(shared); + check_f64(scr_arr_len(a), 3, "release-nothing: a shared release keeps the array"); + scr_arr_release(a); + check(nothing_releases == 0, "release-nothing: teardown calls no element release"); + ScrArr *sparse = scr_arr_new_ops(¬hing_ops, 0); + scr_arr_push_ref(sparse, &kept[0]); + scr_arr_set_ref(sparse, 4000000000.0, &kept[1]); /* out-of-line sparse slot */ + check(scr_arr_ext_of(sparse) != NULL, "release-nothing: a far index makes an ext"); + scr_arr_release(sparse); + check(nothing_releases == 2, "release-nothing: an array with an ext keeps the full teardown"); + ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_push_f64(nums, 1); + scr_arr_release(nums); +#ifdef SCR_RC_AUDIT + check(scr_arr_live_count() == arrays0, "release-nothing: no arrays leaked"); +#endif +} + /* Element ops past the registry's capacity: every descriptor still finds its * own retain and release, and the first ones keep their registry ids. */ static void test_ops_overflow(void) { @@ -2419,6 +2457,7 @@ int main(int argc, char **argv) { test_nested_rc(); test_index_of_includes(); test_ref_elements(); + test_release_nothing_ops(); test_ref_known_count_cycle(&mock_cyc_ops); test_ops_overflow(); test_ref_known_count_cycle(&mock_cyc_ops_late); diff --git a/tests/corpus/7401-program-lifetime-array-teardown.ts b/tests/corpus/7401-program-lifetime-array-teardown.ts new file mode 100644 index 000000000..64e23118a --- /dev/null +++ b/tests/corpus/7401-program-lifetime-array-teardown.ts @@ -0,0 +1,102 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Arrays whose elements need no release when the array is destroyed: +// ProgramLifetime instances (plain and `| undefined` elements), numbers and +// booleans. They are created and dropped by literals, push, slice, filter, +// map, concat, splice, sort and spread, kept in fields, closures and maps, +// nested in other arrays, given holes and far indices, and +// mixed with arrays of ordinary objects, whose elements are released. + +class Node implements ProgramLifetime { + readonly id: number; + next: Node | undefined = undefined; + readonly kids: Node[] = []; + constructor(id: number) { + this.id = id; + } +} + +class Leaf extends Node { + weight = 2; +} + +class Temp { + readonly label: string; + constructor(label: string) { + this.label = label; + } +} + +const nodes: Node[] = []; +for (let i = 0; i < 40; i++) nodes.push(i % 3 === 0 ? new Leaf(i) : new Node(i)); +for (let i = 1; i < nodes.length; i++) nodes[i - 1]!.next = nodes[i]; +for (const n of nodes) for (let k = 1; k <= n.id % 4; k++) n.kids.push(nodes[(n.id + k) % 40]!); + +function ids(list: readonly (Node | undefined)[]): string { + return list.map((n) => (n === undefined ? "-" : String(n.id))).join(","); +} + +// Temporary arrays from every array operation, dropped at once. +let total = 0; +for (let round = 0; round < 200; round++) { + const picked = nodes.filter((n) => n.id % 7 === round % 7); + const mapped = picked.map((n) => n.next); + const joined = picked.concat(nodes.slice(round % 10, (round % 10) + 3)); + const spliced = joined.slice(); + spliced.splice(1, 2, nodes[round % 40]!); + const sorted = [...spliced].sort((a, b) => b.id - a.id); + const optional: (Node | undefined)[] = [nodes[round % 40], undefined, mapped[0]]; + total += picked.length + mapped.length + joined.length + sorted.length + optional.length; + if (round % 50 === 0) console.log(round, ids(sorted), ids(optional), ids(mapped)); +} +console.log("temporaries", total); + +// Numbers and booleans. +let sum = 0; +for (let round = 0; round < 100; round++) { + const values = [round, round * 2, round * 3]; + const flags = values.map((v) => v % 2 === 0); + sum += values.reduce((a, b) => a + b, 0) + flags.filter((f) => f).length; +} +console.log("scalars", sum); + +// Holes and far indices keep the full teardown. +const holey: Node[] = []; +holey[3] = nodes[3]!; +holey[0] = nodes[0]!; +const far: Node[] = [nodes[1]!]; +far[4_000_000_000] = nodes[2]!; +console.log("holey", holey.length, 1 in holey, ids([holey[0], holey[3]])); +console.log("far", far.length, far[4_000_000_000]?.id, far[0]?.id); + +// Kept arrays: a field, a closure, a map, an array of arrays. +class Holder { + items: Node[] = []; +} +const holder = new Holder(); +for (let i = 0; i < 5; i++) holder.items = nodes.slice(i, i + 4); +const later = ((kept: Node[]) => () => ids(kept))(nodes.filter((n) => n instanceof Leaf)); +const byKey = new Map(); +for (let i = 0; i < 10; i++) byKey.set(`k${i % 3}`, nodes.slice(i, i + 2)); +const nested: Node[][] = []; +for (let i = 0; i < 6; i++) nested.push(nodes.slice(i, i + i)); +nested.splice(2, 3); +console.log("field", ids(holder.items)); +console.log("closure", later()); +console.log("map", [...byKey].map(([k, v]) => `${k}:${ids(v)}`).join(" ")); +console.log("nested", nested.map((list) => ids(list)).join(" | ")); + +// Arrays of ordinary objects still release their elements. +let labels = ""; +for (let round = 0; round < 50; round++) { + const temps = [new Temp(`t${round}`), new Temp(`u${round}`)]; + const kept = temps.filter((t) => t.label.endsWith("0")); + labels += kept.map((t) => t.label).join(""); +} +console.log("objects", labels); + +// Kids lists reached through the graph. +let walked = 0; +for (let at: Node | undefined = nodes[0]; at !== undefined; at = at.next) + walked += at.kids.filter((k) => k.id > at!.id).length; +console.log("walked", walked, nodes[39]!.kids.length, (nodes[3] as Leaf).weight); diff --git a/tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json b/tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json new file mode 100644 index 000000000..2741c4d83 --- /dev/null +++ b/tests/harness/corpus-costs/7401-program-lifetime-array-teardown.ts.json @@ -0,0 +1 @@ +{ "plain": 3103, "san": 4147 } From 36209f6d173da92e4d836b9f47b12b3b6150f391 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:24:07 -0500 Subject: [PATCH 065/169] Keep the unreached JavaScript coverage fixture failing to compile The fixture's unreached statement pushed values onto `{ items: [] }`, which mapped to a `number[]` and could not hold them. Since #1881 a JavaScript `[]` literal field is a checked-dynamic array, so the push compiles and the test "a failed unreached JavaScript statement is always explained" found no failed statement. The fixture now starts `items` as `[0]`, a `number[]`, so the push of a boolean onto it still does not compile and reports SC1090 at the same line. --- tests/coverage-fixtures/unreached-js-fence.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/coverage-fixtures/unreached-js-fence.js b/tests/coverage-fixtures/unreached-js-fence.js index 549e633d3..f900e5834 100644 --- a/tests/coverage-fixtures/unreached-js-fence.js +++ b/tests/coverage-fixtures/unreached-js-fence.js @@ -1,8 +1,8 @@ // An exported function nothing on the entry path calls: a statement in it -// that can only fence at runtime still reports its diagnostic, so no -// failed statement goes unexplained. +// that does not compile (a boolean pushed onto the numbers in `items`) +// still reports its diagnostic, so no failed statement goes unexplained. export function collect(values, flag) { - var out = { items: [] }; + var out = { items: [0] }; var x; out.items.push((x = +flag, x * 0 === 0) ? x : flag, !!flag); return out; From 3cace3e785f3d4d5eb6b24ab5c15e771a13be998 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:36:04 -0500 Subject: [PATCH 066/169] Throw Node's TypeError for class constructors called through call/apply `C.call(thisArg, ...)` and `C.apply(thisArg, args)` on a program class that declares no static `call` or `apply` now lower as a call through the boxed class value. It evaluates the arguments in order, applies apply's argument-list checks, and throws `TypeError: Class constructor C cannot be invoked without 'new'` with the runtime class name, as Node does. Before, TypeScript reported SC2020 at compile time and JavaScript threw the standard-library fence's Error at run time. This covers ES5 constructors that call a class as their base (`function Sub() { Base.call(this); }`). Runtime-provided classes (Error, EventEmitter, streams, EventTarget), classes with runtime statics, spread arguments, and receivers other than a name, `this` or a namespace member keep their existing lowering. --- .../src/frontend/lowering/lower-classes.ts | 47 +++++++++++++- .../7480-class-call-without-new.ts.json | 6 ++ .../7481-class-call-without-new.mjs.json | 6 ++ tests/corpus/7480-class-call-without-new.ts | 60 ++++++++++++++++++ tests/corpus/7481-class-call-without-new.mjs | 63 +++++++++++++++++++ .../7480-class-call-without-new.ts.json | 1 + .../7481-class-call-without-new.mjs.json | 1 + 7 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json create mode 100644 tests/corpus/7480-class-call-without-new.ts create mode 100644 tests/corpus/7481-class-call-without-new.mjs create mode 100644 tests/harness/corpus-costs/7480-class-call-without-new.ts.json create mode 100644 tests/harness/corpus-costs/7481-class-call-without-new.mjs.json diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b23697cab..17d48913a 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -6638,7 +6638,7 @@ function staticCallOn( // functions, called directly as `%C.static:m%n` — with the same // through-a-VALUE shadowing fence as plain statics. const gfound = findGenericStaticOn(lowerer, info, access.name.text); - if (!gfound) return null; + if (!gfound) return classConstructorCall(lowerer, call, access, info); if (throughValue && staticShadowBelow(info, access.name.text)) { lowerer.unsupported( "SC1090", @@ -6731,6 +6731,51 @@ function staticCallOn( return { kind: "call", callee: fnName, args, type: found.method.ret, loc }; } +/** `C.call(thisArg, ...)` / `C.apply(thisArg, args)` on a program class that + * declares no such static: Function.prototype.call/apply invoke the class + * constructor without `new`, which throws Node's TypeError "Class + * constructor C cannot be invoked without 'new'" after the arguments are + * evaluated (and, for apply, after the argument list is read). The boxed + * class value's call path throws exactly that. Neither expando statics nor + * writes through untyped values can give a class an own `call` or `apply`. + * The receiver must be one the caller does not evaluate again (a name, + * `this`, or a namespace member). Null otherwise. */ +function classConstructorCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + info: ClassInfo, +): IrExpr | null { + const name = access.name.text; + const receiver = access.expression; + if ( + (name !== "call" && name !== "apply") || + !info.decl || + info.def.runtime || + info.builtinError || + info.builtinEmitter || + info.builtinStream || + info.builtinEventTarget || + info.runtimeStatics?.length || + call.arguments.nodes.some(ts.isSpreadElement) || + !( + ts.isIdentifier(receiver) || + receiver.kind === ts.SyntaxKind.ThisKeyword || + (ts.isPropertyAccessExpression(receiver) && nsMemberIdentOf(lowerer, receiver) !== null) + ) + ) + return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(receiver, DYN), + method: name, + calleeName: ts.getTextOfNode(access), + args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + type: DYN, + loc: locOf(call), + }; +} + /** `C.m(...)` where `m` is a static method that a function-local * JavaScript class defines at runtime (a factory's `class extends base`), * inherited by `C` through its statically known base chain. When the body diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json new file mode 100644 index 000000000..8e5ed83bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7480-class-call-without-new.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json new file mode 100644 index 000000000..7f55a233d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7481-class-call-without-new.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7480-class-call-without-new.ts b/tests/corpus/7480-class-call-without-new.ts new file mode 100644 index 000000000..82b3b8452 --- /dev/null +++ b/tests/corpus/7480-class-call-without-new.ts @@ -0,0 +1,60 @@ +// Function.prototype.call and apply invoke a class constructor without +// `new`, which throws a TypeError naming the class after the arguments are +// evaluated. A declared static `call` answers instead. +class Base { + n = 1; +} +class Derived extends Base {} +class Pair { + left: string; + right: string; + constructor(left: string, right: string) { + this.left = left; + this.right = right; + } +} +class WithCall { + static call(value: number): number { + return value + 1; + } +} + +function report(label: string, run: () => unknown): void { + try { + run(); + console.log(label, "returned"); + } catch (error) { + console.log(label, error instanceof TypeError, (error as Error).message); + } +} + +const receiver = new Base(); +report("call", () => Base.call(receiver)); +report("apply", () => Base.apply(receiver, [])); +report("derived", () => Derived.call(new Derived())); +report("arguments", () => Pair.call(new Pair("a", "b"), "c", "d")); + +const order: string[] = []; +function effect(label: string, value: T): T { + order.push(label); + return value; +} +report("ordered", () => Pair.call(effect("this", new Pair("a", "b")), effect("left", "x"), effect("right", "y"))); +console.log(order.join(",")); + +function local(): void { + class Inner { + x = 2; + } + report("local", () => Inner.call(new Inner())); +} +local(); + +class Statics { + static run(): void { + report("static this", () => this.call(new Statics())); + } +} +Statics.run(); + +console.log(WithCall.call(4)); diff --git a/tests/corpus/7481-class-call-without-new.mjs b/tests/corpus/7481-class-call-without-new.mjs new file mode 100644 index 000000000..4734f0491 --- /dev/null +++ b/tests/corpus/7481-class-call-without-new.mjs @@ -0,0 +1,63 @@ +// An ES5 constructor that calls a class constructor as its base, and the +// other ways of calling a class without `new`, throw Node's TypeError. +class Base { + constructor() { + this.n = 1; + } +} + +function report(label, run) { + try { + run(); + console.log(label, "returned"); + } catch (error) { + console.log(label, error instanceof TypeError, error.message); + } +} + +function Sub() { + Base.call(this); + this.m = 2; +} +report("es5 subclass", () => new Sub()); + +function Applied() { + Base.apply(this, arguments); +} +report("es5 apply", () => new Applied(1, 2)); + +report("apply null", () => Base.apply(null, null)); +report("apply non-object", () => Base.apply(null, 5)); +report("apply array-like", () => Base.apply({}, { length: 1, 0: "a" })); + +const order = []; +function effect(label, value) { + order.push(label); + return value; +} +report("ordered", () => Base.call(effect("this", {}), effect("a", 1), effect("b", 2))); +console.log(order.join(",")); + +function viaValue(C) { + report("value " + C.name, () => C.call({})); +} +class Derived extends Base {} +viaValue(Base); +viaValue(Derived); + +class Configured {} +Configured.defaults = { size: 1 }; +function Wrapped() { + Configured.call(this, Configured.defaults); +} +report("expando statics", () => new Wrapped()); + +const Anonymous = class {}; +report("anonymous", () => Anonymous.call({})); + +class WithCall { + static call(value) { + return "own " + value; + } +} +console.log(WithCall.call(4)); diff --git a/tests/harness/corpus-costs/7480-class-call-without-new.ts.json b/tests/harness/corpus-costs/7480-class-call-without-new.ts.json new file mode 100644 index 000000000..f14d666a4 --- /dev/null +++ b/tests/harness/corpus-costs/7480-class-call-without-new.ts.json @@ -0,0 +1 @@ +{ "plain": 3114, "san": 2571 } diff --git a/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json b/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json new file mode 100644 index 000000000..cecf59f78 --- /dev/null +++ b/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json @@ -0,0 +1 @@ +{ "plain": 2315, "san": 2145 } From ac940ed936ad5c906812fac1b10555c155a52fa9 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:30:16 -0700 Subject: [PATCH 067/169] Support new.target in class constructors Inside a class constructor new.target is the class of the instance under construction. A compiled new allocates the instance with its concrete class before the constructor chain runs, and the class hierarchy is closed, so new.target lowers to a static dispatch over the constructor's class and its descendants (deepest instanceof first). new.target.name reads the exact class's name and new.target === C is an exact-class test, without a class value, so subclasses may construct with other signatures. new.target as a class value requires every subclass to share the constructor's signature. Plain functions and function-local subclasses keep explicit refusals. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-exprs.ts | 13 +- .../src/frontend/lowering/new-target.ts | 219 ++++++++++++++++++ .../7362-new-target-constructors.ts.json | 6 + .../7363-new-target-generic-registry.ts.json | 6 + .../new-target-local-subclass.ts.json | 6 + .../new-target-plain-function.ts.json | 6 + .../new-target-value-signatures.ts.json | 6 + tests/corpus/7362-new-target-constructors.ts | 55 +++++ .../7363-new-target-generic-registry.ts | 32 +++ .../diagnostics/new-target-local-subclass.ts | 5 + .../diagnostics/new-target-plain-function.ts | 3 + .../new-target-value-signatures.ts | 5 + .../new-target-local-subclass.ts.txt | 6 + .../new-target-plain-function.ts.txt | 6 + .../new-target-value-signatures.ts.txt | 6 + .../7362-new-target-constructors.ts.json | 1 + .../7363-new-target-generic-registry.ts.json | 1 + 17 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/new-target.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json create mode 100644 tests/corpus/7362-new-target-constructors.ts create mode 100644 tests/corpus/7363-new-target-generic-registry.ts create mode 100644 tests/diagnostics/new-target-local-subclass.ts create mode 100644 tests/diagnostics/new-target-plain-function.ts create mode 100644 tests/diagnostics/new-target-value-signatures.ts create mode 100644 tests/harness/__snapshots__/new-target-local-subclass.ts.txt create mode 100644 tests/harness/__snapshots__/new-target-plain-function.ts.txt create mode 100644 tests/harness/__snapshots__/new-target-value-signatures.ts.txt create mode 100644 tests/harness/corpus-costs/7362-new-target-constructors.ts.json create mode 100644 tests/harness/corpus-costs/7363-new-target-generic-registry.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ac8b02b69..2bde5f2aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,4 +1,10 @@ import { keyedHelperName } from "./stable-names.js"; +import { + isNewTarget, + lowerNewTarget, + lowerNewTargetComparison, + lowerNewTargetName, +} from "./new-target.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; @@ -713,7 +719,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isPrefixUnaryExpression(expr)) return lowerer.lowerPrefixUnary(expr); // `x++` / `x--` in expression position: yields the OLD value. if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false); - if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr); + if (ts.isBinaryExpression(expr)) + return lowerNewTargetComparison(lowerer, expr) ?? lowerer.lowerBinary(expr); if (ts.isCallExpression(expr)) { // `h.return?.()` on a helper object: the method exists, so the // optional call is the call. @@ -729,13 +736,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isElementAccessExpression(expr)) return lowerer.lowerElementAccess(expr); if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); - if (ts.isPropertyAccessExpression(expr)) return lowerPropertyAccessExpr(lowerer, expr, loc); + if (ts.isPropertyAccessExpression(expr)) + return lowerNewTargetName(lowerer, expr) ?? lowerPropertyAccessExpr(lowerer, expr, loc); // Meta-properties, named: `new.target` reflects HOW a function was // invoked (compiled functions are never constructors of themselves — // no runtime invocation record exists), and `import.meta`/ // `import.defer` are module-loader surface a native binary does not // carry. + if (isNewTarget(expr)) return lowerNewTarget(lowerer, expr); if (ts.isMetaProperty(expr)) { const name = expr.keywordToken === ts.SyntaxKind.NewKeyword ? "new.target" : `import.${expr.name.text}`; diff --git a/packages/compiler/src/frontend/lowering/new-target.ts b/packages/compiler/src/frontend/lowering/new-target.ts new file mode 100644 index 000000000..8b5f5858a --- /dev/null +++ b/packages/compiler/src/frontend/lowering/new-target.ts @@ -0,0 +1,219 @@ +/* `new.target`. Compiled code has no invocation record, but inside a class + * constructor `new.target` is always the class of the object under + * construction: a compiled `new` allocates the instance with its concrete + * class before any constructor of the chain runs, and every constructor of + * the chain receives that instance as `this`. The program's class + * hierarchy is closed, so the value is a static dispatch over the + * constructor's class and its descendants: the deepest class the instance + * is an instance of is its exact class. + * + * The checker rejects `new.target` in methods, accessors, static blocks and + * field initializers. Plain functions (constructible in JavaScript) keep + * the refusal. */ +import * as ts from "../ts7/adapter.js"; +import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { locOf } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; +import type { ClassInfo } from "./lower-classes.js"; +import { classValueRef } from "./lower-classes.js"; + +export function isNewTarget(node: ts.Node): node is ts.MetaProperty { + return ( + ts.isMetaProperty(node) && + node.keywordToken === ts.SyntaxKind.NewKeyword && + node.name.text === "target" + ); +} + +/** Where `new.target` is bound: a class constructor, or a plain function + * (refused). */ +type Binding = + | { kind: "constructor"; declaration: ts.ConstructorDeclaration } + | { kind: "function" }; + +function bindingOf(node: ts.Node): Binding { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isArrowFunction(p)) continue; + if (ts.isConstructorDeclaration(p)) return { kind: "constructor", declaration: p }; + if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return { kind: "function" }; + } + return { kind: "function" }; +} + +interface Dispatch { + info: ClassInfo; + /** The constructor's class and its descendants, deepest first. */ + classes: ClassInfo[]; + self: IrExpr | null; +} + +function depth(info: ClassInfo): number { + let d = 0; + for (let c = info.base; c; c = c.base) d++; + return d; +} + +/** The classes `new.target` can denote in the constructor being lowered, + * or null after a diagnostic. */ +function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { + const binding = bindingOf(expr); + if (binding.kind !== "constructor") return null; + const info = lowerer.currentClass; + const cls = binding.declaration.parent; + if (!info || (info.decl !== null && info.decl !== cls)) { + lowerer.unsupported( + "SC1090", + expr, + "'new.target' in this constructor (its class has no lowering here)", + ); + return null; + } + const classes = [info]; + for (const other of lowerer.classes.values()) { + if (other === info || other.generic) continue; + if (lowerer.isSubclassOf(other.def.name, info.def.name)) classes.push(other); + } + for (const c of classes) { + if (c.localClass) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' where '${c.def.jsName ?? c.def.name}' is a function-local class (each evaluation creates a new class)`, + ); + return null; + } + } + classes.sort((a, b) => depth(b) - depth(a)); + let self: IrExpr | null = null; + if (classes.length > 1) { + const thisLocal = lowerer.resolveThis(expr); + if (!thisLocal || thisLocal.type.kind !== "object") { + lowerer.unsupported( + "SC1090", + expr, + "'new.target' where the constructor's 'this' has no class representation", + ); + return null; + } + self = varRef(thisLocal.id, thisLocal.type, locOf(expr)); + } + return { info, classes, self }; +} + +/** A conditional chain selecting `arm(D)` for the exact class D of the + * instance under construction. */ +function select( + dispatch: Dispatch, + type: IrType, + loc: SrcLoc, + arm: (info: ClassInfo) => IrExpr, +): IrExpr { + const { classes, self } = dispatch; + let result = arm(classes[classes.length - 1]!); + for (let i = classes.length - 2; i >= 0; i--) { + const c = classes[i]!; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: self!, className: c.def.name, type: BOOL, loc }, + then: arm(c), + else_: result, + type, + loc, + }; + } + return result; +} + +function refuseFunction(lowerer: Lowerer, expr: ts.MetaProperty): never { + return lowerer.unsupported( + "SC1090", + expr, + "'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor)", + ); +} + +/** `new.target` as a value. */ +export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr { + const loc = locOf(expr); + if (bindingOf(expr).kind === "function") refuseFunction(lowerer, expr); + const dispatch = dispatchOf(lowerer, expr)!; + const type: IrType = { kind: "classval", className: dispatch.info.def.name }; + for (const c of dispatch.classes) { + if (c !== dispatch.info && !lowerer.ctorAbiEquals(c, dispatch.info)) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' as a value where '${c.def.jsName ?? c.def.name}' constructs with a different signature than '${dispatch.info.def.jsName ?? dispatch.info.def.name}' (read 'new.target.name' or compare 'new.target' with a class instead)`, + ); + } + } + return select(dispatch, type, loc, (c) => + lowerer.coerceToExpected(classValueRef(lowerer, c, expr), type), + ); +} + +/** `new.target.name`: the exact class's name, without materializing a + * class value (subclasses may construct with other signatures). */ +export function lowerNewTargetName( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || access.name.text !== "name") return null; + let target: ts.Expression = access.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!isNewTarget(target) || bindingOf(target).kind !== "constructor") return null; + const dispatch = dispatchOf(lowerer, target); + if (!dispatch) return null; + const loc = locOf(access); + return select(dispatch, STRING, loc, (c) => ({ + kind: "libCall", + fn: "class.name", + args: [classValueRef(lowerer, c, target)], + type: STRING, + loc, + })); +} + +/** `new.target === C` (and `!==`, `==`, `!=`, either operand order) with a + * program class named by the other operand: an exact-class test. */ +export function lowerNewTargetComparison( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const op = expr.operatorToken.kind; + const negated = + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken; + if ( + !negated && + op !== ts.SyntaxKind.EqualsEqualsEqualsToken && + op !== ts.SyntaxKind.EqualsEqualsToken + ) + return null; + const strip = (e: ts.Expression): ts.Expression => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; + }; + const left = strip(expr.left); + const right = strip(expr.right); + const target = isNewTarget(left) ? left : isNewTarget(right) ? right : null; + if (!target || bindingOf(target).kind !== "constructor") return null; + const other = target === left ? right : left; + if (!ts.isIdentifier(other)) return null; + // A constructor's new.target is never undefined. + if (other.text === "undefined" && (lowerer.typeOf(other).flags & ts.TypeFlags.Undefined) !== 0) + return { kind: "boolLit", value: negated, type: BOOL, loc: locOf(expr) }; + const symbol = lowerer.resolveValueSymbol(other); + const named = symbol ? lowerer.classBySymbol.get(symbol) : undefined; + if (!named || named.generic || named.localClass) return null; + const dispatch = dispatchOf(lowerer, target); + if (!dispatch) return null; + const loc = locOf(expr); + return select(dispatch, BOOL, loc, (c) => ({ + kind: "boolLit", + value: (c === named) !== negated, + type: BOOL, + loc, + })); +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json new file mode 100644 index 000000000..f38c901e0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7362-new-target-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json new file mode 100644 index 000000000..a1c8cb535 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7363-new-target-generic-registry.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json new file mode 100644 index 000000000..4f597a8a2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-local-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json new file mode 100644 index 000000000..9f424457c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-plain-function.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json new file mode 100644 index 000000000..249f8f4bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-value-signatures.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7362-new-target-constructors.ts b/tests/corpus/7362-new-target-constructors.ts new file mode 100644 index 000000000..9e94200bf --- /dev/null +++ b/tests/corpus/7362-new-target-constructors.ts @@ -0,0 +1,55 @@ +// new.target in class constructors: the class under construction, through +// inherited and explicit subclass constructors, Error subclasses, arrows, +// identity comparisons and class values. +abstract class AppError extends Error { + abstract readonly status: number; + constructor(message: string) { + super(message); + this.name = new.target.name; + } +} +class NotFound extends AppError { + readonly status = 404; +} +class Invalid extends AppError { + readonly status = 400; + readonly field: string; + constructor(field: string, detail: number) { + super(`bad ${field} ${detail}`); + this.field = field; + } +} +class Strict extends Invalid {} +const errors: AppError[] = [new NotFound("missing"), new Invalid("isbn", 3), new Strict("title", 1)]; +for (const e of errors) console.log(e.name, e.status, e.message, String(e), e instanceof Invalid); + +class Shape { + readonly kind: string; + readonly exact: boolean; + constructor() { + this.kind = new.target.name; + this.exact = new.target === Shape; + const probe = () => new.target !== Circle; + console.log("probe", probe(), new.target == Square, new.target === undefined); + } + describe(): string { + return `${this.kind}${this.exact ? " (exact)" : ""}`; + } +} +class Circle extends Shape {} +class Square extends Shape { + constructor() { + super(); + console.log("square ctor sees", new.target.name); + } +} +for (const s of [new Shape(), new Circle(), new Square()]) console.log(s.describe()); + +class Node2 { + static make(): Node2 { return new this(); } + readonly origin: typeof Node2; + constructor() { this.origin = new.target; } + where(): string | undefined { return typeof (() => 1)() === "number" ? String(this.origin.name) : undefined; } +} +class Leaf extends Node2 {} +console.log(new Node2().origin === Node2, new Leaf().origin === Leaf, new Leaf().where(), Node2.make().origin.name); diff --git a/tests/corpus/7363-new-target-generic-registry.ts b/tests/corpus/7363-new-target-generic-registry.ts new file mode 100644 index 000000000..17866d143 --- /dev/null +++ b/tests/corpus/7363-new-target-generic-registry.ts @@ -0,0 +1,32 @@ +// new.target in generic classes and as a construction registry key. +class Box { + readonly label: string; + readonly value: T; + constructor(value: T) { + this.value = value; + this.label = `${new.target.name}<${typeof value}>`; + } +} +class NumBox extends Box { + constructor(n: number) { + super(n * 2); + } +} +const a = new Box("s"); +const b = new NumBox(4); +console.log(a.label, b.label, b.value); + +class Registry { + static readonly created: string[] = []; + constructor() { + Registry.created.push(new.target.name); + if (new.target === Registry) console.log("plain registry"); + } +} +class Users extends Registry {} +class Books extends Registry {} +new Users(); +new Books(); +new Registry(); +new Books(); +console.log(Registry.created.join(",")); diff --git a/tests/diagnostics/new-target-local-subclass.ts b/tests/diagnostics/new-target-local-subclass.ts new file mode 100644 index 000000000..a5f153049 --- /dev/null +++ b/tests/diagnostics/new-target-local-subclass.ts @@ -0,0 +1,5 @@ +// A function-local subclass makes a new class per evaluation, so the +// base constructor's new.target has no static dispatch. +class A { constructor() { console.log(new.target.name); } } +function make() { class Local extends A {} return new Local(); } +make(); diff --git a/tests/diagnostics/new-target-plain-function.ts b/tests/diagnostics/new-target-plain-function.ts new file mode 100644 index 000000000..da592e084 --- /dev/null +++ b/tests/diagnostics/new-target-plain-function.ts @@ -0,0 +1,3 @@ +// new.target in a plain function has no invocation record to read. +function F(this: unknown) { console.log(new.target === undefined); } +F.call(null); diff --git a/tests/diagnostics/new-target-value-signatures.ts b/tests/diagnostics/new-target-value-signatures.ts new file mode 100644 index 000000000..af0c906e5 --- /dev/null +++ b/tests/diagnostics/new-target-value-signatures.ts @@ -0,0 +1,5 @@ +// new.target as a class value needs every subclass to construct with the +// base's signature; B's constructor takes a parameter A's does not. +class A { readonly t: typeof A; constructor() { this.t = new.target; } } +class B extends A { constructor(readonly n: number) { super(); } } +console.log(new B(1).t.name); diff --git a/tests/harness/__snapshots__/new-target-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt new file mode 100644 index 000000000..0f1d465b1 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-local-subclass.ts:3:39 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet + + 2 | // base constructor's new.target has no static dispatch. + 3 | class A { constructor() { console.log(new.target.name); } } + | ^~~~~~~~~~ + 4 | function make() { class Local extends A {} return new Local(); } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-plain-function.ts.txt b/tests/harness/__snapshots__/new-target-plain-function.ts.txt new file mode 100644 index 000000000..5005f5915 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-plain-function.ts.txt @@ -0,0 +1,6 @@ +new-target-plain-function.ts:2:41 - error SC1090: 'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor) is not supported yet + + 1 | // new.target in a plain function has no invocation record to read. + 2 | function F(this: unknown) { console.log(new.target === undefined); } + | ^~~~~~~~~~ + 3 | F.call(null); \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-value-signatures.ts.txt b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt new file mode 100644 index 000000000..9c38e4d51 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt @@ -0,0 +1,6 @@ +new-target-value-signatures.ts:3:58 - error SC1090: 'new.target' as a value where 'B' constructs with a different signature than 'A' (read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet + + 2 | // base's signature; B's constructor takes a parameter A's does not. + 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } } + | ^~~~~~~~~~ + 4 | class B extends A { constructor(readonly n: number) { super(); } } \ No newline at end of file diff --git a/tests/harness/corpus-costs/7362-new-target-constructors.ts.json b/tests/harness/corpus-costs/7362-new-target-constructors.ts.json new file mode 100644 index 000000000..2c7339a24 --- /dev/null +++ b/tests/harness/corpus-costs/7362-new-target-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 6000, "san": 26000 } diff --git a/tests/harness/corpus-costs/7363-new-target-generic-registry.ts.json b/tests/harness/corpus-costs/7363-new-target-generic-registry.ts.json new file mode 100644 index 000000000..eceea988d --- /dev/null +++ b/tests/harness/corpus-costs/7363-new-target-generic-registry.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 23500 } From f210b1102abf9c57e229d7610567cca9dd45c287 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 16:38:34 -0700 Subject: [PATCH 068/169] Run zod 4's core constructor factory in the dynamic tier zod 4 builds every schema through `$constructor`, a factory of JavaScript patterns the dynamic tier refused or answered wrongly. Each is fixed generally: - `"key" in BuiltinCtor` (`"captureStackTrace" in Error`) folds to Node's answer from a table of the builtin constructor globals' keys. - A class whose heritage evaluates to Object or Error at runtime (`class D extends (params?.Parent ?? Object)`): Object means no heritage; Error links Error.prototype and super() installs message, cause and stack. - `Object.defineProperty` on a class value boxes to its constructor, and a JavaScript class targeted by defineProperty gets a live static table. - A Symbol.hasInstance defined on a function's own table answers untyped `instanceof`; an error constructor held as a value answers by error kind. - A builtin error constructor stored in a slot of its class value type (`{ Parent: Error }`) is the class value, not a checked cast that fails. - A function hoisted above a local class it references captures the class binding as a TDZ box the class statement initializes. - Names a program stores on the global object read back from it instead of folding to undefined. - A module namespace whose exports never change converts to an untyped object (`{ ...ns }`). - An untyped call of a String-only method name (`x.trim()`) dispatches on the receiver's runtime kind, so objects defining that member work. Co-Authored-By: Claude Opus 5.5 --- .../builtins/global-namespace-keys.ts | 123 +++++++++++++++++ .../lowering/expressions/identifiers.ts | 7 + .../lowering/expressions/property-access.ts | 41 ++++++ .../lowering/expressions/relational.ts | 11 ++ .../src/frontend/lowering/lower-calls.ts | 13 +- .../src/frontend/lowering/lower-classes.ts | 43 +++++- .../src/frontend/lowering/lower-stmts.ts | 46 +++++++ .../compiler/src/frontend/lowering/lowerer.ts | 24 ++++ .../lowering/module-namespace-elements.ts | 82 ++++++++++- .../7480-builtin-constructor-in.js.json | 6 + ...lass-runtime-heritage-object-error.js.json | 6 + ...ine-property-function-class-values.js.json | 6 + .../7483-constructor-factory-traits.js.json | 6 + ...sted-function-captures-later-class.js.json | 6 + .../7485-global-object-own-properties.js.json | 6 + ...otype-accessor-string-method-names.js.json | 6 + .../main.js.json | 8 ++ packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_dyn_builtin_class.c | 2 + packages/runtime/src/scr_json.c | 128 ++++++++++++++++++ packages/runtime/src/scr_symbol.c | 5 + tests/corpus/7480-builtin-constructor-in.js | 9 ++ ...481-class-runtime-heritage-object-error.js | 24 ++++ ...2-define-property-function-class-values.js | 35 +++++ .../corpus/7483-constructor-factory-traits.js | 58 ++++++++ ...4-hoisted-function-captures-later-class.js | 23 ++++ .../7485-global-object-own-properties.js | 22 +++ ...-prototype-accessor-string-method-names.js | 53 ++++++++ .../7487-namespace-spread-untyped/extra.js | 2 + .../7487-namespace-spread-untyped/main.js | 11 ++ .../7487-namespace-spread-untyped/shapes.js | 4 + .../7480-builtin-constructor-in.js.json | 1 + ...lass-runtime-heritage-object-error.js.json | 1 + ...ine-property-function-class-values.js.json | 1 + .../7483-constructor-factory-traits.js.json | 1 + ...sted-function-captures-later-class.js.json | 1 + .../7485-global-object-own-properties.js.json | 1 + ...otype-accessor-string-method-names.js.json | 1 + .../main.js.json | 1 + 39 files changed, 821 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json create mode 100644 tests/corpus/7480-builtin-constructor-in.js create mode 100644 tests/corpus/7481-class-runtime-heritage-object-error.js create mode 100644 tests/corpus/7482-define-property-function-class-values.js create mode 100644 tests/corpus/7483-constructor-factory-traits.js create mode 100644 tests/corpus/7484-hoisted-function-captures-later-class.js create mode 100644 tests/corpus/7485-global-object-own-properties.js create mode 100644 tests/corpus/7486-prototype-accessor-string-method-names.js create mode 100644 tests/corpus/7487-namespace-spread-untyped/extra.js create mode 100644 tests/corpus/7487-namespace-spread-untyped/main.js create mode 100644 tests/corpus/7487-namespace-spread-untyped/shapes.js create mode 100644 tests/harness/corpus-costs/7480-builtin-constructor-in.js.json create mode 100644 tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json create mode 100644 tests/harness/corpus-costs/7482-define-property-function-class-values.js.json create mode 100644 tests/harness/corpus-costs/7483-constructor-factory-traits.js.json create mode 100644 tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json create mode 100644 tests/harness/corpus-costs/7485-global-object-own-properties.js.json create mode 100644 tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json create mode 100644 tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json diff --git a/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts new file mode 100644 index 000000000..bb364ee55 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts @@ -0,0 +1,123 @@ +/* The string-keyed properties Node 24 exposes on its builtin constructor + * and namespace globals, own and inherited, so `"key" in Error` (a feature + * probe such as `"captureStackTrace" in Error`) answers statically. The + * global must be the unshadowed builtin; compiled programs cannot add + * properties to these objects, so the answer is fixed. */ + +const FUNCTION_PROTOTYPE = [ + "length", + "name", + "arguments", + "caller", + "constructor", + "apply", + "bind", + "call", + "toString", +]; +const OBJECT_PROTOTYPE = [ + "constructor", + "__defineGetter__", + "__defineSetter__", + "hasOwnProperty", + "__lookupGetter__", + "__lookupSetter__", + "isPrototypeOf", + "propertyIsEnumerable", + "toString", + "valueOf", + "__proto__", + "toLocaleString", +]; +const CONSTRUCTOR_OWN = ["length", "name", "prototype"]; +const ERROR_OWN = [ + ...CONSTRUCTOR_OWN, + "captureStackTrace", + "prepareStackTrace", + "isError", + "stackTraceLimit", +]; + +/** Own string keys per global, beyond what the prototype chain adds. */ +const CONSTRUCTORS: Readonly> = { + Error: ERROR_OWN, + // The native error constructors inherit from Error. + TypeError: ERROR_OWN, + RangeError: ERROR_OWN, + SyntaxError: ERROR_OWN, + ReferenceError: ERROR_OWN, + EvalError: ERROR_OWN, + URIError: ERROR_OWN, + Object: [ + ...CONSTRUCTOR_OWN, + "assign", + "getOwnPropertyDescriptor", + "getOwnPropertyDescriptors", + "getOwnPropertyNames", + "getOwnPropertySymbols", + "hasOwn", + "is", + "preventExtensions", + "seal", + "create", + "defineProperties", + "defineProperty", + "freeze", + "getPrototypeOf", + "setPrototypeOf", + "isExtensible", + "isFrozen", + "isSealed", + "keys", + "entries", + "fromEntries", + "values", + "groupBy", + ], + Array: [...CONSTRUCTOR_OWN, "isArray", "from", "fromAsync", "of"], + Promise: [ + ...CONSTRUCTOR_OWN, + "all", + "allSettled", + "any", + "race", + "resolve", + "reject", + "withResolvers", + "try", + ], + Number: [ + ...CONSTRUCTOR_OWN, + "isFinite", + "isInteger", + "isNaN", + "isSafeInteger", + "parseFloat", + "parseInt", + "MAX_VALUE", + "MIN_VALUE", + "NaN", + "NEGATIVE_INFINITY", + "POSITIVE_INFINITY", + "MAX_SAFE_INTEGER", + "MIN_SAFE_INTEGER", + "EPSILON", + ], + String: [...CONSTRUCTOR_OWN, "fromCharCode", "fromCodePoint", "raw"], + Map: [...CONSTRUCTOR_OWN, "groupBy"], + Set: CONSTRUCTOR_OWN, + WeakMap: CONSTRUCTOR_OWN, + WeakSet: CONSTRUCTOR_OWN, + Boolean: CONSTRUCTOR_OWN, + Function: CONSTRUCTOR_OWN, + Date: [...CONSTRUCTOR_OWN, "now", "parse", "UTC"], + BigInt: [...CONSTRUCTOR_OWN, "asUintN", "asIntN"], +}; + +/** Whether Node answers `key in ` with true, or null when `name` is + * not a tabled global. */ +export function builtinGlobalHasKey(name: string, key: string): boolean | null { + const own = CONSTRUCTORS[name]; + if (own === undefined) return null; + return own.includes(key) || FUNCTION_PROTOTYPE.includes(key) || OBJECT_PROTOTYPE.includes(key); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index eca3c33f5..e0f43cc4a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,4 +1,5 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ +import { predeclareForwardClass } from "../lower-stmts.js"; import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js"; import * as ts from "../../ts7/adapter.js"; @@ -862,6 +863,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: { const sym = lowerer.resolveValueSymbol(expr); const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; + // A local class referenced from a function hoisted above its statement: + // its TDZ box, holding the class as a checked-dynamic constructor. + if (!classInfo && sym && predeclareForwardClass(lowerer, sym)) { + const local = lowerer.resolveLocal(expr); + if (local) return { kind: "varRef", localId: local.id, type: local.type, loc }; + } // A GENERIC class name whose CONTEXTUAL type pins one instantiation // (`const B: new (v: string) => Counter = Counter`): the // value is that instantiation's class object — the generic-fn diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 5df3c7fe0..d1b9795d5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -553,6 +553,19 @@ export function lowerPropertyAccessExpr( expr.name.text, ) ) { + // A name the program stores on the global object + // (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from + // the global object's own properties. + const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression)); + if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) { + return { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }; + } return dynUndefinedExpr(loc); } // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex @@ -4490,3 +4503,31 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { } return count; } + +const globalThisWrites = new WeakMap>(); + +/** The property names the program assigns on the global object, through + * `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */ +function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set { + const known = globalThisWrites.get(lowerer); + if (known) return known; + const names = new Set(); + globalThisWrites.set(lowerer, names); + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue; + ts.walkPreorder(sf, (node) => { + if ( + ts.isBinaryExpression(node) && + (node.operatorToken.kind === ts.SyntaxKind.EqualsToken || + node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken || + node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken || + node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) && + ts.isPropertyAccessExpression(node.left) && + lowerer.typeOf(node.left.expression) === globalType + ) + names.add(node.left.name.text); + return undefined; + }); + } + return names; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 759687ff0..24a582746 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -51,6 +51,7 @@ import { isJsSourceFile, locOf } from "../../program.js"; import { checkedClassInstanceOf } from "../class-construction.js"; import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; import { globalSymbolKey } from "./global-symbols.js"; +import { builtinGlobalHasKey } from "../builtins/global-namespace-keys.js"; import { foldedStringKeyOf } from "./object-literals.js"; import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; @@ -1426,6 +1427,16 @@ export function lowerInExpression( "'in' with computed (non-literal) keys (a const whose type is one string literal folds; a string-typed key answers over index-signature record receivers)", ); } + // A feature probe on a builtin constructor global (`"captureStackTrace" + // in Error`): the global is a fixed object, so the answer is static. + { + let recvNode = expr.right; + while (ts.isParenthesizedExpression(recvNode)) recvNode = recvNode.expression; + if (ts.isIdentifier(recvNode) && lowerer.isStdlibGlobal(recvNode, recvNode.text)) { + const has = builtinGlobalHasKey(recvNode.text, key); + if (has !== null) return { kind: "boolLit", value: has, type: BOOL, loc }; + } + } // A receiver re-read (the static folds below) is safe for plain reads // AND for side-effect-free expressions (an object literal of literals // — the `"a" in { a: true }` shape). diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c0406e77b..42e3a20cf 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3804,11 +3804,12 @@ function lowerDynReceiverMethodCall( if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { // An object-typed receiver the member is missing from (a JavaScript // module object whose members are assigned in a function body, - // `common.repeat(" ", n)`) holds a string or an object with that + // `common.repeat(" ", n)`), or an untyped receiver (an object whose + // prototype defines `trim`), holds a string or an object with that // member: dispatch on the receiver's runtime kind. if ( - !checkerUntyped && - (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !call.questionDotToken && !access.questionDotToken && !call.arguments.nodes.some(ts.isSpreadElement) @@ -8233,6 +8234,12 @@ function lowerObjectStaticCall( ...(fnName !== null ? { fnName } : {}), }; } + // A class value (`Object.defineProperty(Definition, "name", { value })`) + // boxes to its constructor function, whose own-property table holds the + // definition for every alias of the class. + if (target?.type.kind === "classval" && lowerer.dynConvertible(target.type)) { + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; + } const descriptorPrimitive = member === "getOwnPropertyDescriptor" && target && diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b23697cab..d283067cd 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -5,6 +5,7 @@ import { isDatePrototypeMethod, lowerDateSuperMethodCall, } from "./builtins/date-classes.js"; +import { predeclareForwardClass } from "./lower-stmts.js"; import { lowerDateStaticCall } from "./builtins/dates.js"; import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; @@ -2355,6 +2356,7 @@ export function collectClassShapeInner( const checkedStatics = isJsSourceFile(ts.getSourceFileOfNode(decl)) && ((base && hasRuntimeStatics(base)) || + definedThroughReflection(lowerer, decl) || decl.members.nodes.some( (member) => ts.isAccessor(member) && @@ -5476,7 +5478,12 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) const display = info.def.name.replace(/^%|^%m\d+\./, ""); fenceDecorationThrows(lowerer, info, blame); if (info.localClass && ts.isIdentifier(blame)) { - const stored = lowerer.resolveLocal(blame); + let stored = lowerer.resolveLocal(blame); + // A reference from a function hoisted above the class statement. + if (!stored) { + const symbol = lowerer.resolveValueSymbol(blame); + if (symbol && predeclareForwardClass(lowerer, symbol)) stored = lowerer.resolveLocal(blame); + } if (stored) return lowerer.coerceToExpected( { kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) }, @@ -11152,3 +11159,37 @@ function genericFieldFnNodeOf( } return null; } + +/** Whether a JavaScript class's own binding is the target of + * `Object.defineProperty`/`defineProperties` in its file + * (`Object.defineProperty(Definition, "name", { value })`): its constructor + * then needs a live own-property table. */ +function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean { + const name = decl.name; + const sf = ts.getSourceFileOfNode(decl); + if (name === undefined || !sf.text.includes("definePropert")) return false; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (symbol === undefined) return false; + let found = false; + ts.walkPreorder(sf, (node) => { + if (found) return "stop"; + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + (node.expression.name.text === "defineProperty" || + node.expression.name.text === "defineProperties") && + lowerer.stdlibGlobalMember(node.expression, "Object") !== null + ) { + const target = node.arguments.nodes[0]; + if ( + target !== undefined && + ts.isIdentifier(target) && + target.text === name.text && + lowerer.checker.getSymbolAtLocation(target) === symbol + ) + found = true; + } + return undefined; + }); + return found; +} diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ea5ef72ac..0f1b097c2 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1010,6 +1010,44 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b return false; } +/** The CLASS-DECLARATION twin of predeclareForwardCapture: a function + * hoisted above a local `class D` (`function make() { return new D(); }` + * reached through an earlier function) captures the class binding, which + * is in its temporal dead zone until the class statement runs. The class + * cannot be collected yet (its heritage may name bindings declared between + * here and the statement), so the binding is pre-declared as a TDZ box + * holding the class as a checked-dynamic constructor; the class statement + * later initializes it, and every reference reads it from there. */ +export function predeclareForwardClass(lowerer: Lowerer, symbol: ts.Symbol): boolean { + const decl = symbol.valueDeclaration; + if (!decl || !ts.isClassDeclaration(decl) || decl.name === undefined) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return false; + for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { + const entry = lowerer.activeStmtLists[i]!; + const idx = entry.stmts.indexOf(decl); + if (idx < 0) continue; + if (idx < entry.index || entry.ctx === lowerer.ctx || entry.ctx.tdzPredeclared.has(symbol)) + return false; + const name = decl.name.text; + const count = entry.ctx.localCounters.get(name) ?? 0; + entry.ctx.localCounters.set(name, count + 1); + const local: IrLocal = { + id: `${name}.${count}`, + name, + type: DYN, + mutable: false, + boxed: true, + tdz: true, + }; + entry.ctx.locals.push(local); + entry.frame.set(symbol, local); + entry.out.push({ kind: "varDecl", localId: local.id, init: null, loc: locOf(decl) }); + entry.ctx.tdzPredeclared.set(symbol, local); + return true; + } + return false; +} + /** The FUNCTION-DECLARATION twin of predeclareForwardCapture: JS hoists a * nested `function f() {}` to scope entry — the binding is LIVE from the * first statement, so a reference lexically above the declaration in the @@ -1529,6 +1567,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isClassDeclaration(stmt)) { if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations"); const info = lowerer.lowerClassExpressionInfo(stmt); + // An earlier hoisted function captured the class: its TDZ box exists. + const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); + const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; + if (pre) { + lowerer.ctx.tdzPredeclared.delete(symbol!); + const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN); + return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) }; + } const type: IrType = { kind: "classval", className: info.def.name }; const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true); // Static members of a function-local class run while the class value is diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 93c6246d9..83ac404d4 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,5 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; +import { moduleNamespaceObject } from "./module-namespace-elements.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; @@ -9445,6 +9446,22 @@ export class Lowerer { } if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr)) return this.divergentValue(expr, expected); + // A builtin error constructor global stored in a slot of its class value + // type (`{ Parent: Error }`): the class value itself, which a checked + // cast of the dynamic constructor value could not produce. + if ( + expected.kind === "classval" && + expr.kind === "libCall" && + expr.fn === "dyn.globalConstructor" && + expr.args[0]?.kind === "strLit" && + `%${expr.args[0].value}` === expected.className + ) { + const info = this.classes.get(expected.className); + if (info?.builtinError && info.valueCtorParams !== undefined) { + this.defineClassValueConstruct(info); + return { kind: "classRef", className: info.def.name, type: expected, loc: expr.loc }; + } + } // process.stdout/stderr into a general Writable slot (general-streams.ts). const stdioStream = stdioIntoGeneralStream(this, expr, expected); if (stdioStream) return this.coerceToExpected(stdioStream, expected); @@ -9551,6 +9568,13 @@ export class Lowerer { // constructor. `require("events")` is the EventEmitter class itself, // so its module binding is too (`ReleaseEmitter = EventEmitter` over // `const EventEmitter = require("events")`). + if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) { + const object = moduleNamespaceObject( + this, + expr as IrExpr & { type: IrType & { kind: "moduleNs" } }, + ); + if (object) return object; + } if ( expr.type.kind === "moduleNs" && expr.type.moduleId === "builtin:events" && diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index d2425750b..df40573da 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -1,6 +1,13 @@ import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; -import { DYN, STRING, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { + DYN, + STRING, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { locOf } from "../program.js"; import { newFnCtx, type Lowerer } from "./lowerer.js"; @@ -88,3 +95,76 @@ export function lowerModuleNamespaceElement( }; return { kind: "call", callee: name, args: [receiver, text], type: DYN, loc }; } + +/** A module namespace taken as an untyped value (`{ ...ns }`, `{ iso: ns }` + * in JavaScript): an object holding each value export, keyed in the + * namespace's sorted key order. The copy is exact while every export is a + * binding that never changes after initialization (const, function or + * class); null for other namespaces, which keep their fence. */ +export function moduleNamespaceObject( + lowerer: Lowerer, + ns: IrExpr & { type: IrType & { kind: "moduleNs" } }, +): IrExpr | null { + const source = lowerer.sourceFileOfModuleNamespace(ns.type); + if (!source) return null; + const symbol = lowerer.checker.getSymbolAtLocation(source); + if (!symbol) return null; + const exports: { key: string; identifier: ts.Identifier }[] = []; + let exact = true; + symbol.getExports().forEach((exported, key) => { + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + if (!(target.flags & ts.SymbolFlags.Value)) return; + const declaration = target.valueDeclaration; + const identifier = declaration?.name as ts.Node | undefined; + const immutable = + declaration !== undefined && + (ts.isFunctionDeclaration(declaration) || + ts.isClassDeclaration(declaration) || + (ts.isVariableDeclaration(declaration) && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0)); + if (!identifier || !ts.isIdentifier(identifier) || !immutable) { + exact = false; + return; + } + exports.push({ key: String(key), identifier }); + }); + if (!exact) return null; + exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const name = `%module.object:${ns.type.moduleId}`; + const loc = ns.loc; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + const previousClass = lowerer.currentClass; + lowerer.currentClass = null; + lowerer.fnStack.push(context); + try { + const fields = exports.map(({ key, identifier }) => ({ + key: strLit(key, loc), + value: lowerer.lowerExprExpecting(identifier, DYN), + })); + if (fields.some((field) => field.value.type.kind !== "dyn")) return null; + const fn: IrFunction = { + name, + params: [], + returnType: DYN, + locals: [...context.locals], + body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }], + loc, + }; + lowerer.liftedFns.push(fn); + } finally { + lowerer.fnStack.pop(); + lowerer.currentClass = previousClass; + } + } + const call: IrExpr = { kind: "call", callee: name, args: [], type: DYN, loc }; + if (ns.kind === "moduleNsRef") return call; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: ns, loc }], + result: call, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json new file mode 100644 index 000000000..efb9dd725 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7480-builtin-constructor-in.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json new file mode 100644 index 000000000..d0d390662 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7481-class-runtime-heritage-object-error.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json new file mode 100644 index 000000000..d1d97ba4f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7482-define-property-function-class-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json new file mode 100644 index 000000000..c3fa9964f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7483-constructor-factory-traits.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json new file mode 100644 index 000000000..515b9bf97 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7484-hoisted-function-captures-later-class.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json new file mode 100644 index 000000000..aa1affb3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7485-global-object-own-properties.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json new file mode 100644 index 000000000..0cf87c443 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7486-prototype-accessor-string-method-names.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json new file mode 100644 index 000000000..0c768ef66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7487-namespace-spread-untyped/shapes.js", + "/tests/corpus/7487-namespace-spread-untyped/extra.js", + "/tests/corpus/7487-namespace-spread-untyped/main.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 74b710dc9..63d7cb1b7 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -863,6 +863,7 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators); * for-of protocol reads it. +1, or NULL with the error pending. */ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); +void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when @@ -1270,6 +1271,7 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); +int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee); /* JS String() over a dyn value (arrays join, [object Object], the error * encoding renders "name: message"); borrowed, result +1, never throws. */ ScrStr *scr_dyn_display(const ScrDyn *d); diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 2f947048a..0647a65c0 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -1581,6 +1581,8 @@ bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; } + int hooked = scr_dyn_has_instance_hook(value, callee); + if (hooked >= 0) return hooked == 1; ScrDyn *prototype = bc_get(callee, "prototype"); if (!prototype) return false; bool found = false; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cf658313f..57c9e4df4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -189,6 +189,19 @@ void scr_dyn_install_async_iterator_symbol(ScrDyn *key) { scr_atexit(scr_async_iterator_symbol_cleanup); } +/* Symbol.hasInstance, installed when the program first evaluates it: a + * function can only define a hook after reading the symbol. */ +static SCR_TL ScrDyn *scr_has_instance_symbol; +static void scr_has_instance_symbol_cleanup(void) { + scr_dyn_release(scr_has_instance_symbol); + scr_has_instance_symbol = NULL; +} +void scr_dyn_install_has_instance_symbol(ScrDyn *key) { + if (scr_has_instance_symbol) return; + scr_has_instance_symbol = scr_dyn_retain(key); + scr_atexit(scr_has_instance_symbol_cleanup); +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -3983,12 +3996,49 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD /* `value instanceof callee` for an untyped constructor value. `refusal`, when * given, is the site's SC1090 message for a runtime constructor with no * instance test. */ +/* A function whose own-property table defines Symbol.hasInstance + * (`Object.defineProperty(F, Symbol.hasInstance, { value })`): 1 or 0 by the + * truthiness of the method's answer with `this` bound to F, -1 when F + * defines none, or 0 with the exception pending. Functions without a table + * use the ordinary prototype test. */ +int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) { + if (!scr_has_instance_symbol || callee->kind != SCR_DYN_FUNC || !scr_closure_identity(callee->v.fn.clo)->props) + return -1; + ScrDyn *hook = scr_dyn_symbol_key_get(callee, scr_has_instance_symbol, true); + if (scr_exc_pending()) { + scr_dyn_release(hook); + return 0; + } + if (!hook || hook->kind != SCR_DYN_FUNC) { + scr_dyn_release(hook); + return -1; + } + scr_dyn_this_push_dyn((ScrDyn *)callee); + ScrDyn *argument = (ScrDyn *)value; + ScrDyn *result = scr_dyn_call(hook, &argument, 1, "Symbol.hasInstance"); + scr_dyn_this_pop(); + scr_dyn_release(hook); + if (!result) return 0; + bool truthy = scr_dyn_truthy(result); + scr_dyn_release(result); + return truthy ? 1 : 0; +} + bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; } + int hooked = scr_dyn_has_instance_hook(value, callee); + if (hooked >= 0) return hooked == 1; + /* An error constructor held as a value (`inst instanceof params.Parent` + * with Parent === Error): the error kind's interval, and for Error itself + * objects inheriting Error.prototype. */ + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + if (!scr_error_ctor_values[i] || callee->v.fn.clo != scr_error_ctor_values[i]->v.fn.clo) continue; + return scr_dyn_err_instanceof(value, (double)i) || (i == 0 && scr_dyn_inherits_error_prototype(value)); + } 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); for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { @@ -5021,7 +5071,43 @@ ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { return scr_exc_pending() ? NULL : scr_dyn_retain(constructor); } +/* Defines `key` on target as a writable, configurable, non-enumerable data + * property holding value (borrowed), as Error's constructor installs its + * message and cause. */ +static void scr_dyn_define_hidden(ScrDyn *target, const ScrStr *key, ScrDyn *value) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(value)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *defined = scr_dyn_define_property(target, name, descriptor); + scr_dyn_release(name); + scr_dyn_release(descriptor); + scr_dyn_release(defined); +} + +/* A builtin constructor a class expression's runtime heritage can name: + * 1 for Object, 2 for Error, 0 otherwise. */ +static int scr_dyn_builtin_base_kind(const ScrDyn *constructor) { + if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || !constructor->v.fn.sig || + !constructor->v.fn.name || strcmp(constructor->v.fn.sig, "native:constructor") != 0) + return 0; + if (strcmp(constructor->v.fn.name, "Object") == 0) return 1; + if (strcmp(constructor->v.fn.name, "Error") == 0 && constructor->v.fn.clo->fn == (void *)scr_error_ctor_names[0]) + return 2; + return 0; +} + ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { + /* `class extends (Base ?? Object)`: Object's instances are ordinary + * objects, laid out like a class without heritage (undefined); Error's + * inherit Error.prototype. */ + switch (scr_dyn_builtin_base_kind(constructor)) { + case 1: return scr_dyn_retain(scr_dyn_undefined()); + case 2: return scr_dyn_error_prototype(); + default: break; + } if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || scr_closure_identity(constructor->v.fn.clo)->function_kind != 4) { static const char message[] = "Class extends value is not a native ordinary constructor"; @@ -5038,6 +5124,48 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { } void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *arguments) { + switch (scr_dyn_builtin_base_kind(constructor)) { + case 1: + /* super() through Object keeps the receiver the derived constructor + * created (OrdinaryCreateFromConstructor). */ + return; + case 2: { + /* super(message, options) through Error: the message and cause become + * non-enumerable own properties, then the stack (its text alone, as for + * scriptc's other errors). */ + size_t argc = arguments->v.arr.len; + ScrDyn *const *argv = arguments->v.arr.items; + if (argc > 0 && argv[0]->kind != SCR_DYN_UNDEF) { + ScrStr *text = scr_dyn_string_coerce_js(argv[0]); + if (!text) return; + ScrDyn *value = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new("message", 7); + scr_dyn_define_hidden(receiver, key, value); + scr_str_release(key); + scr_dyn_release(value); + if (scr_exc_pending()) return; + } + if (argc > 1 && argv[1]->kind == SCR_DYN_OBJ && scr_dyn_find_entry(argv[1], "cause", 5)) { + ScrDyn *cause = scr_dyn_obj_get(argv[1], "cause", 5); + ScrStr *key = scr_str_new("cause", 5); + scr_dyn_define_hidden(receiver, key, cause ? cause : scr_dyn_undefined()); + scr_str_release(key); + if (scr_exc_pending()) return; + } + ScrStr *text = scr_dyn_error_string(receiver); + if (!text) return; + ScrDyn *stack = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new("stack", 5); + scr_dyn_define_hidden(receiver, key, stack); + scr_str_release(key); + scr_dyn_release(stack); + return; + } + default: + break; + } scr_dyn_this_push_dyn(receiver); ScrDyn *result = scr_dyn_call(constructor, arguments->v.arr.items, arguments->v.arr.len, "super"); scr_dyn_this_pop(); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 304f2544f..647c31dc3 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -85,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_iterator_symbol(key); scr_dyn_release(key); } + if (name->len == 11 && !memcmp(name->data, "hasInstance", 11)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_has_instance_symbol(key); + scr_dyn_release(key); + } if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_async_iterator_symbol(key); diff --git a/tests/corpus/7480-builtin-constructor-in.js b/tests/corpus/7480-builtin-constructor-in.js new file mode 100644 index 000000000..781a00105 --- /dev/null +++ b/tests/corpus/7480-builtin-constructor-in.js @@ -0,0 +1,9 @@ +// Feature probes on builtin constructor globals answer like Node. +const E = "captureStackTrace" in Error ? Error : null; +console.log(E === Error, "stackTraceLimit" in Error, "nope" in Error); +console.log("captureStackTrace" in TypeError, "keys" in Object, "fromAsync" in Array, "withResolvers" in Promise); +console.log("call" in Map, "hasOwnProperty" in Set, "groupBy" in Map, "groupBy" in Set, "now" in Date); +const capture = "captureStackTrace" in Error ? Error.captureStackTrace : () => {}; +const target = {}; +capture(target); +console.log(typeof target.stack); diff --git a/tests/corpus/7481-class-runtime-heritage-object-error.js b/tests/corpus/7481-class-runtime-heritage-object-error.js new file mode 100644 index 000000000..13d59dc63 --- /dev/null +++ b/tests/corpus/7481-class-runtime-heritage-object-error.js @@ -0,0 +1,24 @@ +function make(name, params) { + const Parent = (params && params.Parent) || Object; + class Definition extends Parent { + describe() { + return `${name}:${this.kind}`; + } + } + return Definition; +} +const A = make("A"); +const a = new A(); +a.kind = "plain"; +console.log(typeof a, a instanceof A, a.describe(), JSON.stringify(a), Object.keys(a)); +const E = make("E", { Parent: Error }); +const e = new E("boom"); +e.kind = "error"; +console.log(e instanceof E, e.message, typeof e.stack, e.describe(), Object.keys(e), String(e)); +const Parent = E; +console.log(e instanceof Parent, new E() .message === "", Object.prototype.hasOwnProperty.call(new E(), "message")); +try { + throw new E("thrown"); +} catch (err) { + console.log(err instanceof E, err.message); +} diff --git a/tests/corpus/7482-define-property-function-class-values.js b/tests/corpus/7482-define-property-function-class-values.js new file mode 100644 index 000000000..7a682adab --- /dev/null +++ b/tests/corpus/7482-define-property-function-class-values.js @@ -0,0 +1,35 @@ +const desc = { value: undefined, enumerable: false }; +function factory(name) { + class Definition {} + Object.defineProperty(Definition, "name", { value: name }); + function init(inst, def) { + if (!inst._meta) { + desc.value = { def, traits: new Set() }; + try { + Object.defineProperty(inst, "_meta", desc); + } finally { + desc.value = undefined; + } + } + inst._meta.traits.add(name); + } + function _(def) { + const inst = this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, Symbol.hasInstance, { + value: (inst) => !!(inst && inst._meta && inst._meta.traits.has(name)), + }); + Object.defineProperty(_, "name", { value: name }); + return { ctor: _, Definition }; +} +const { ctor: Thing, Definition } = factory("Thing"); +console.log(Definition.name, Thing.name, typeof Thing.init); +const t = new Thing({ a: 1 }); +console.log(Object.keys(t), JSON.stringify(t), t._meta.def.a, [...t._meta.traits]); +console.log(t instanceof Thing, {} instanceof Thing); +const plain = {}; +Thing.init(plain, { b: 2 }); +console.log(plain instanceof Thing, Object.keys(plain), plain._meta.def.b); diff --git a/tests/corpus/7483-constructor-factory-traits.js b/tests/corpus/7483-constructor-factory-traits.js new file mode 100644 index 000000000..e022f0672 --- /dev/null +++ b/tests/corpus/7483-constructor-factory-traits.js @@ -0,0 +1,58 @@ +const _desc = { value: undefined, enumerable: false }; +function $constructor(name, initializer, params) { + const proto = {}; + function Internals(def) { + this.def = def; + this.constr = _; + this.traits = new Set(); + } + Internals.prototype = proto; + function init(inst, def) { + if (!inst._zod) { + _desc.value = new Internals(def); + try { + Object.defineProperty(inst, "_zod", _desc); + } finally { + _desc.value = undefined; + } + } else if (inst._zod.traits.has(name)) { + return; + } + inst._zod.traits.add(name); + initializer(inst, def); + } + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, Symbol.hasInstance, { + value: (inst) => { + if (params?.Parent && inst instanceof params.Parent) return true; + return inst?._zod?.traits?.has(name); + }, + }); + Object.defineProperty(_, "name", { value: name }); + return _; +} +const $Base = $constructor("$Base", (inst, def) => { + inst.kind = def.kind; + inst._zod.parse = (v) => (typeof v === def.kind ? { value: v } : { issues: [`expected ${def.kind}`] }); +}); +const $Str = $constructor("$Str", (inst, def) => { + $Base.init(inst, def); + inst.min = def.min; +}); +const $Err = $constructor("$Err", (inst, def) => { + inst.issues = def; + inst.message = JSON.stringify(def); +}, { Parent: Error }); +const s = new $Str({ kind: "string", min: 2 }); +console.log(s.kind, s.min, [...s._zod.traits], s instanceof $Str, s instanceof $Base, Object.keys(s)); +console.log(JSON.stringify(s._zod.parse("ab")), JSON.stringify(s._zod.parse(3))); +const e = new $Err(["bad"]); +console.log(e instanceof $Err, e.issues, e.message, $Str.name, $Err.name); diff --git a/tests/corpus/7484-hoisted-function-captures-later-class.js b/tests/corpus/7484-hoisted-function-captures-later-class.js new file mode 100644 index 000000000..2272882af --- /dev/null +++ b/tests/corpus/7484-hoisted-function-captures-later-class.js @@ -0,0 +1,23 @@ +function f(params) { + function early() { + return make; + } + const Parent = params?.Parent ?? Object; + class D extends Parent {} + function make() { + return new D(); + } + return early()(); +} +console.log(typeof f(), typeof f({ Parent: Error })); +function g() { + function early() { + return make; + } + class E {} + function make() { + return new E(); + } + return early()(); +} +console.log(typeof g()); diff --git a/tests/corpus/7485-global-object-own-properties.js b/tests/corpus/7485-global-object-own-properties.js new file mode 100644 index 000000000..a9a72be18 --- /dev/null +++ b/tests/corpus/7485-global-object-own-properties.js @@ -0,0 +1,22 @@ +var _a; +globalThis.__direct = { n: 1 }; +console.log(typeof globalThis.__direct, globalThis.__direct.n); + +(_a = globalThis).__alias ?? (_a.__alias = { n: 2 }); +console.log(typeof globalThis.__alias); +const g = globalThis; +g.__viaConst = { n: 3 }; +console.log(typeof globalThis.__viaConst); +globalThis.__nullish ??= { n: 4 }; +console.log(typeof globalThis.__nullish); + +(_a = globalThis).__demo_config ?? (_a.__demo_config = {}); +const globalConfig = globalThis.__demo_config; +function config(next) { + if (next) Object.assign(globalConfig, next); + return globalConfig; +} +config({ localeError: (issue) => `bad ${issue}` }); +console.log(typeof globalConfig, typeof globalConfig.localeError, config().localeError("x")); +const pp = globalThis.__demo_config?.postProcessor; +console.log(pp === undefined, globalThis.__demo_config === globalConfig); diff --git a/tests/corpus/7486-prototype-accessor-string-method-names.js b/tests/corpus/7486-prototype-accessor-string-method-names.js new file mode 100644 index 000000000..9920717ea --- /dev/null +++ b/tests/corpus/7486-prototype-accessor-string-method-names.js @@ -0,0 +1,53 @@ +function own(inst, key, value, enumerable = true) { + Object.defineProperty(inst, key, { configurable: true, writable: true, enumerable, value }); + return value; +} +function defineBound(proto, key, fn) { + Object.defineProperty(proto, key, { + configurable: true, + get() { + return this == null ? fn : own(this, key, fn.bind(this)); + }, + set(value) { + own(this, key, value); + }, + }); +} +function members(proto, table) { + for (const key in table) { + const desc = Object.getOwnPropertyDescriptor(table, key); + if (desc.get) Object.defineProperty(proto, key, { ...desc, enumerable: false }); + else defineBound(proto, key, desc.value); + } +} +function Schema(def) { + this.def = def; +} +let installed = false; +function make(def) { + const inst = new Schema(def); + if (!installed) { + installed = true; + members(Object.getPrototypeOf(inst), { + min(n) { return make({ ...this.def, min: n }); }, + max(n) { return make({ ...this.def, max: n }); }, + trim() { return make({ ...this.def, trim: true }); }, + get size() { return Object.keys(this.def).length; }, + }); + } + return inst; +} +const s = make({ kind: "string" }).min(1).max(200).trim(); +console.log(JSON.stringify(s.def), s.size, Object.keys(s), typeof s.trim); +const detached = make({ kind: "x" }).max; +console.log(JSON.stringify(detached(3).def)); +const proto2 = {}; +Object.defineProperty(proto2, "aaa", { configurable: true, get() { return () => "custom-aaa"; } }); +Object.defineProperty(proto2, "trim", { configurable: true, get() { return () => "custom-trim"; } }); +Object.defineProperty(proto2, "trimx", { configurable: true, get() { return () => "custom-trimx"; } }); +Object.defineProperty(proto2, "slice", { configurable: true, get() { return () => "custom-slice"; } }); +const o2 = Object.create(proto2); +console.log(o2.aaa()); +console.log(o2.trimx()); +console.log(o2.slice()); +console.log(o2.trim()); diff --git a/tests/corpus/7487-namespace-spread-untyped/extra.js b/tests/corpus/7487-namespace-spread-untyped/extra.js new file mode 100644 index 000000000..8c62c2dd0 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/extra.js @@ -0,0 +1,2 @@ +export const tag = (s) => `<${s}>`; +export function zeta() { return "z"; } diff --git a/tests/corpus/7487-namespace-spread-untyped/main.js b/tests/corpus/7487-namespace-spread-untyped/main.js new file mode 100644 index 000000000..021c00ac9 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/main.js @@ -0,0 +1,11 @@ +import * as shapes from "./shapes.js"; +import * as extra from "./extra.js"; +// A namespace spread into an untyped object (zod's local +// `const z = { ..._schemas, ..._checks, iso: _iso }`). +function untyped(value) { + return value; +} +const z = untyped({ ...shapes, ...extra, extra }); +console.log(Object.keys(z).join(",")); +for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2)); +console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size); diff --git a/tests/corpus/7487-namespace-spread-untyped/shapes.js b/tests/corpus/7487-namespace-spread-untyped/shapes.js new file mode 100644 index 000000000..a8b5cbfc2 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/shapes.js @@ -0,0 +1,4 @@ +export const VERSION = "1.2"; +export function circle(r) { return { kind: "circle", area: Math.PI * r * r }; } +export function square(s) { return { kind: "square", area: s * s }; } +export class Box { constructor(size) { this.size = size; } } diff --git a/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7485-global-object-own-properties.js.json b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } From b386e9195805895dff7050f6b5b1702dd352152b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:50:12 -0500 Subject: [PATCH 069/169] Compile instanceof against constructors held as values --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/class-dynamic-dispatch.ts | 128 ++++++++++++------ .../src/frontend/lowering/dispatch-sites.ts | 1 + .../lowering/expressions/relational.ts | 63 ++++++++- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/int32-slots.ts | 1 + .../main.ts.json | 7 + ...anceof-builtin-constructor-values.mjs.json | 6 + ...24-instanceof-class-values-native.mjs.json | 6 + ...25-instanceof-dynamic-constructors.ts.json | 6 + packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_json.c | 47 +++++-- .../errors.cjs | 24 ++++ .../6349-instanceof-subclass-capsules/main.ts | 27 ++++ ...-instanceof-builtin-constructor-values.mjs | 13 ++ .../6924-instanceof-class-values-native.mjs | 19 +++ .../6925-instanceof-dynamic-constructors.ts | 64 +++++++++ .../main.ts.json | 1 + ...anceof-builtin-constructor-values.mjs.json | 1 + ...24-instanceof-class-values-native.mjs.json | 1 + ...25-instanceof-dynamic-constructors.ts.json | 1 + 22 files changed, 364 insertions(+), 58 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json create mode 100644 tests/corpus/6349-instanceof-subclass-capsules/errors.cjs create mode 100644 tests/corpus/6349-instanceof-subclass-capsules/main.ts create mode 100644 tests/corpus/6923-instanceof-builtin-constructor-values.mjs create mode 100644 tests/corpus/6924-instanceof-class-values-native.mjs create mode 100644 tests/corpus/6925-instanceof-dynamic-constructors.ts create mode 100644 tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json create mode 100644 tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json create mode 100644 tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json create mode 100644 tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 0b6041034..e8d4e01c6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1555,6 +1555,7 @@ export const LIB_FN_SYMS: Record = { "dyn.nameClassPrototype": "scr_dyn_name_class_prototype", "dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties", "dyn.getPrototype": "scr_dyn_get_prototype", + "dyn.getPrototypeForIdentity": "scr_dyn_get_prototype_for_identity", "dyn.setPrototype": "scr_dyn_set_prototype", "dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names", "dyn.ownKeys": "scr_dyn_own_keys", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 8a4a1e417..b18615ef6 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -350,7 +350,7 @@ export function classInstanceOf( localId: "candidate", value: { kind: "libCall", - fn: "dyn.getPrototype", + fn: "dyn.getPrototypeForIdentity", args: [candidate], type: DYN, loc, @@ -862,6 +862,9 @@ export class ClassDynamicDispatch { private deletedNames: Set | null | undefined; private readonly boxed = new Set(); private readonly boxedConstructors = new Set(); + /** Every class a boxed value was statically typed as, runtime error + * classes included: the value's capsule can be any of its subclasses. */ + private readonly boxedStatic = new Set(); private readonly dispatches = new Map(); private readonly properties = new Map(); private readonly computed = new Map< @@ -906,8 +909,12 @@ export class ClassDynamicDispatch { private descriptorHelper: IrFunction | null = null; private prototypeHelper: IrFunction | null = null; private readonly prototypeClasses = new Set(); - private prototypeIdentityHelper: IrFunction | null = null; - private readonly prototypeIdentityClasses = new Set(); + /** The prototype reads of identity walks, by libCall, and the classes + * each one answers for boxed instances. */ + private readonly prototypeIdentityHelpers = new Map< + "dyn.getPrototype" | "dyn.getPrototypeForIdentity", + { fn: IrFunction; classes: Set } + >(); private readonly instancePrototypes = new Map }>(); private readonly checkedCasts = new Map }>(); /** The for-in key helpers, by libCall (dyn and native string[] keys). */ @@ -956,6 +963,7 @@ export class ClassDynamicDispatch { if (seenTypes.has(key)) return; seenTypes.add(key); if (type.kind === "object") { + this.boxedStatic.add(type.className); if (isDynTypedRefType(type)) this.boxed.add(type.className); } else if (type.kind === "classval") this.boxedConstructors.add(type.className); else if (type.kind === "array") discover(type.elem); @@ -1087,33 +1095,6 @@ export class ClassDynamicDispatch { }); changed = true; } - if (this.prototypeIdentityHelper && !this.prototypeIdentityClasses.has(className)) { - this.prototypeIdentityClasses.add(className); - const loc = this.prototypeIdentityHelper.loc; - const type: IrType = { kind: "object", className }; - const value = varRef("value", DYN, loc); - const prototype = classPrototypeData(lowerer, info, loc, { - kind: "dynCheck", - value, - type, - loc, - }); - if (prototype) - this.prototypeIdentityHelper.body.unshift({ - kind: "if", - cond: { - kind: "libCall", - fn: "dyn.typedRefIs", - args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], - type: BOOL, - loc, - }, - then: [{ kind: "return", value: prototype, loc }], - else_: null, - loc, - }); - changed = true; - } if ( this.serializesClasses && !this.jsonPrototypes.has(className) && @@ -1181,6 +1162,53 @@ export class ClassDynamicDispatch { } } } + // The runtime boxes a class instance as its exact class, so a value + // boxed at a base (or Error) static type reaches the identity views as + // any subclass's capsule. + for (const helper of this.prototypeIdentityHelpers.values()) + for (const [className, info] of lowerer.classes) { + if (helper.classes.has(className)) continue; + if (info.builtinEmitter || info.builtinStream || info.builtinError) continue; + if (!isDynTypedRefType({ kind: "object", className })) continue; + if ( + !this.boxed.has(className) && + ![...this.boxedStatic].some((base) => lowerer.isSubclassOf(className, base)) + ) + continue; + helper.classes.add(className); + const loc = helper.fn.loc; + const type: IrType = { kind: "object", className }; + const value = varRef("value", DYN, loc); + // A class with no prototype data in its chain (one extending + // Error, or with no methods) has no prototype a program can name: + // a fresh object stands in, so an identity comparison or an + // instanceof walk (through Object.prototype to null) answers false + // instead of reaching the reflection refusal. + const standIn: IrExpr = { kind: "dynObjLit", fields: [], type: DYN, loc }; + const prototype = hasClassPrototypeData(info) + ? classPrototypeData(lowerer, info, loc, { + kind: "dynCheck", + value, + type, + loc, + }) + : standIn; + if (prototype) + helper.fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: prototype, loc }], + else_: null, + loc, + }); + changed = true; + } for (const plan of this.propertyReceivers(lowerer, false, false)) { const info = plan.info; const className = info.def.name; @@ -1518,29 +1546,41 @@ export class ClassDynamicDispatch { } if ( expr.kind === "libCall" && - expr.fn === "dyn.getPrototype" && + (expr.fn === "dyn.getPrototype" || expr.fn === "dyn.getPrototypeForIdentity") && expr.args[0]?.kind !== "dynObjLit" ) { if (expr.prototypeIdentityOnly) { - if (!this.prototypeIdentityHelper) { + let helper = this.prototypeIdentityHelpers.get(expr.fn); + if (!helper) { const loc = expr.loc; - this.prototypeIdentityHelper = { - name: "%dyn.class.prototypeIdentity", - params: [{ localId: "value", name: "value", type: DYN }], - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - returnType: DYN, - loc, - body: [ - { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, - ], + helper = { + fn: { + name: + expr.fn === "dyn.getPrototype" + ? "%dyn.class.prototypeIdentity" + : "%dyn.class.prototypeChainIdentity", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: DYN, + loc, + body: [ + { + kind: "return", + value: { ...expr, args: [varRef("value", DYN, loc)] }, + loc, + }, + ], + }, + classes: new Set(), }; - lowerer.liftedFns.push(this.prototypeIdentityHelper); - this.generated.add(this.prototypeIdentityHelper); + this.prototypeIdentityHelpers.set(expr.fn, helper); + lowerer.liftedFns.push(helper.fn); + this.generated.add(helper.fn); changed = true; } return { kind: "call", - callee: this.prototypeIdentityHelper.name, + callee: helper.fn.name, args: expr.args, type: DYN, loc: expr.loc, diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index c8a8d6ba6..1dcffc811 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -80,6 +80,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.reflectGet", "dyn.reflectSet", ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 759687ff0..ac8fdc96b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -57,6 +57,44 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; +/** The class value an `instanceof` right-hand side holds. A JavaScript + * object's property holding a program class (`holder.type`) reads as a + * checked-dynamic value that the checker types as the class: it converts + * to the class value. Runtime-provided classes keep their own lowering. */ +function classValueOperand( + lowerer: Lowerer, + right: ts.Expression, + type: IrType & { kind: "classval" }, +): IrExpr { + if ( + !isJsSourceFile(ts.getSourceFileOfNode(right)) || + ts.isIdentifier(right) || + lowerer.classes.get(type.className)?.def.runtime + ) + return lowerer.lowerExpr(right); + const value = lowerer.lowerExprExpecting(right, DYN); + if (value.type.kind !== "dyn") return value; + return { kind: "dynCheck", value, type, loc: locOf(right) }; +} + +/** The value kinds that never hold an instance of a program class. */ +const NO_CLASS_INSTANCE_KINDS: ReadonlySet = new Set([ + "f64", + "bigint", + "date", + "string", + "bool", + "array", + "map", + "set", + "regex", + "bytes", + "symbol", + "promise", + "undefinedT", + "nullT", +]); + /** `x instanceof C` for a program-declared class C. When x's static * class and C are both in extends-hierarchies the test is dynamic — the * O(1) preorder-interval check against the vtable (`instanceOf` node). @@ -427,7 +465,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "'instanceof' through a runtime-provided class value on an untyped value (give the operand a class type first)", ); } - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); return classInstanceOf(lowerer, left, targetInfo, loc, classValue); } // A union of class instances against a runtime class value (`new K()` @@ -436,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const tested = unionClassValueInstanceOf(lowerer, left, expr.right, targetInfo, loc); if (tested) return tested; } + // A value whose type holds no class instance (a primitive, a native + // collection, a date, a regex, an array) is never an instance + // of a program class: false, after both operands evaluate. + if (!targetInfo.def.runtime && NO_CLASS_INSTANCE_KINDS.has(left.type.kind)) { + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); + return { + kind: "seqExpr", + stmts: [ + { kind: "exprStmt", expr: left, loc }, + { kind: "exprStmt", expr: classValue, loc }, + ], + result: { kind: "boolLit", value: false, type: BOOL, loc }, + type: BOOL, + loc, + }; + } if (left.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -450,7 +504,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc (lowerer.inHierarchy(targetInfo) && lowerer.inHierarchy(lhsInfo)) || (targetInfo.localClass !== undefined && lhsInfo.localClass !== undefined) ) { - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); const writableTest = writableClassValueTest( @@ -488,8 +542,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "statically-decided 'instanceof' on computed operands (bind the values to variables first)", ); } + // A checked-dynamic right-hand side (a constructor read from a table, a + // structural constructor type, a builtin constructor held as `any`): + // the runtime's instanceof over the value, in both source languages. const constructor = tryLowerExpression(lowerer, expr.right); - if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { + if (constructor?.type.kind === "dyn") { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return checkedClassInstanceOf( lowerer, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index eb3f8c6ab..503ea94a9 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -630,6 +630,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreate", "dyn.objCreateWithProperties", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.setPrototype", // Error.captureStackTrace: Node's TypeError for a primitive target, and // the error string conversion of name and message diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8edb223b3..7af1d4ef9 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3749,6 +3749,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, "dyn.getPrototype": { argTypes: [DYN], result: DYN }, + // dyn.getPrototype for walks that compare each prototype with a program + // class's prototype: native builtin values end the walk (null). + "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, "dyn.ownKeys": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index ac0747276..ec2ebd3ab 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -365,6 +365,7 @@ const DYN_READ_ONLY = new Set([ "dyn.forInStable", "dyn.getOwnPropertyNames", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.hasKey", "dyn.hasOwn", "dyn.in", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6b31ee9ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs", + "/tests/corpus/6349-instanceof-subclass-capsules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..a2184f9a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6923-instanceof-builtin-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..54090ccc3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6924-instanceof-class-values-native.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..20085dc0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6925-instanceof-dynamic-constructors.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 74b710dc9..d9a230276 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -917,6 +917,7 @@ void *scr_dyn_obj_source_cast(const ScrDyn *d, ScrDyn *scr_dyn_new_obj_null_proto(void); ScrDyn *scr_dyn_obj_create(ScrDyn *prototype); ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors); +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object); ScrDyn *scr_dyn_get_prototype(ScrDyn *object); ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object); ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cf658313f..43596272c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2147,6 +2147,26 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { return NULL; } +/* scr_dyn_get_prototype for a walk that compares each prototype with a + * program class's prototype (instanceof against a class value): a native + * Date, RegExp, Map, Set, WeakMap or WeakSet, or a promise, has only builtin + * prototypes on its chain, and no program class extends these (a class + * extending Date makes instances of its own), so the walk ends there with + * null. */ +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object) { + if (object->kind == SCR_DYN_PROMISE) return scr_dyn_new_null(); + if (object->kind == SCR_DYN_HANDLE) { + switch (object->v.handle.tag) { + case SCR_DYNH_DATE: case SCR_DYNH_REGEXP: case SCR_DYNH_MAP: case SCR_DYNH_SET: + case SCR_DYNH_WEAK_MAP: case SCR_DYNH_WEAK_SET: + return scr_dyn_new_null(); + default: + break; + } + } + return scr_dyn_get_prototype(object); +} + ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { /* A primitive receiver is returned unchanged when the prototype is null or * any object or function; nothing is mutated, so the prototype's kind is the @@ -3956,27 +3976,32 @@ ScrDyn *scr_array_buffer_constructor(void) { /* `value instanceof C` for C a global constructor held as a value * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the - * value's native kind, or -1 when callee is not one of them, or for Object - * and for class instances, which these kinds cannot answer (except Date, - * whose subclasses' instances are known). A typed record or array is none - * of these kinds. */ + * value's native kind, or -1 when callee is not one of them, or for Object. + * A typed record or array is none of these kinds, and neither is an + * instance of a program class: a class can extend Date (answered through + * scr_dyn_date_instance_is), and extending RegExp, Map, Set, WeakMap, + * WeakSet or Promise refuses where the class is defined. The error + * constructors (scr_dyn_error_constructor) answer -1. */ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || strcmp(callee->v.fn.name, "Object") == 0) return -1; /* A native Date, or an instance of a class declared `extends Date`. */ if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value); - if (value->kind == SCR_DYN_TYPED_REF) - return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; static const struct { const char *name; int tag; } kinds[] = { - {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, }; - if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; - for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { - if (strcmp(callee->v.fn.name, kinds[i].name) == 0) - return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + bool promise = strcmp(callee->v.fn.name, "Promise") == 0; + int tag = -1; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) tag = kinds[i].tag; + if (value->kind == SCR_DYN_TYPED_REF) { + if (strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value)) return 0; + return (promise || tag >= 0) && strncmp(value->v.typed_ref.type_key, "object:", 7) == 0 ? 0 : -1; } + if (promise) return value->kind == SCR_DYN_PROMISE; + if (tag >= 0) return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == tag; return -1; } diff --git a/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs new file mode 100644 index 000000000..bf32f8550 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs @@ -0,0 +1,24 @@ +'use strict'; +class UndiciError extends Error { + constructor(message) { + super(message); + this.name = 'UndiciError'; + this.code = 'UND_ERR'; + } + get kind() { + return 'undici'; + } +} +class SocketError extends UndiciError { + constructor(message) { + super(message); + this.name = 'SocketError'; + } +} +class HTTPParserError extends Error { + constructor(message) { + super(message); + this.name = 'HTTPParserError'; + } +} +module.exports = { UndiciError, SocketError, HTTPParserError }; diff --git a/tests/corpus/6349-instanceof-subclass-capsules/main.ts b/tests/corpus/6349-instanceof-subclass-capsules/main.ts new file mode 100644 index 000000000..59a545198 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/main.ts @@ -0,0 +1,27 @@ +// `instanceof` against a JavaScript class reads the prototype chain of a +// value that crossed into the dynamic tier at a base static type (here +// Error): the value is its exact class at runtime, including a subclass +// without methods of its own, which answers false rather than throwing. +import errors from "./errors.cjs"; + +function describe(err: unknown): string { + if (err instanceof errors.UndiciError) return `undici ${err.name}`; + if (err instanceof Error) return `error ${err.name}`; + return "other"; +} +function isSocket(err: unknown): boolean { + return err instanceof errors.SocketError; +} +function fail(kind: number): Error { + if (kind === 0) return new errors.HTTPParserError("bad chunk"); + if (kind === 1) return new errors.SocketError("closed"); + return new RangeError("range"); +} +for (let kind = 0; kind < 3; kind++) { + const err: Error = fail(kind); + console.log(describe(err), isSocket(err)); +} +const base: Error = new errors.UndiciError("base"); +console.log(describe(base), isSocket(base)); +const rejected = Promise.reject(fail(0)); +rejected.catch((err: unknown) => console.log("rejected", describe(err))); diff --git a/tests/corpus/6923-instanceof-builtin-constructor-values.mjs b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs new file mode 100644 index 000000000..929b048e3 --- /dev/null +++ b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs @@ -0,0 +1,13 @@ +// `instanceof` against builtin constructors held as values, with instances of +// program classes and native values on the left. +class Dog { + constructor(name) { + this.name = name; + } +} +class Puppy extends Dog {} +const kinds = [Map, Set, RegExp, Promise, WeakMap, WeakSet, Date]; +const values = []; +values.push(new Dog("rex"), new Puppy("bit"), new Map(), new Set(), /x/g, Promise.resolve(1)); +values.push(new WeakMap(), new WeakSet(), new Date(0), { name: "plain" }, [1, 2]); +for (const C of kinds) console.log(C.name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); diff --git a/tests/corpus/6924-instanceof-class-values-native.mjs b/tests/corpus/6924-instanceof-class-values-native.mjs new file mode 100644 index 000000000..82e8bdcf2 --- /dev/null +++ b/tests/corpus/6924-instanceof-class-values-native.mjs @@ -0,0 +1,19 @@ +// `instanceof` against program classes held as values, with native builtin +// values on the left: their prototype chains hold no program class. +class Animal { + constructor(name) { + this.name = name; + } +} +class Dog extends Animal {} +class Cat extends Animal {} +const classes = { Animal: Animal, Dog: Dog, Cat: Cat }; +const values = []; +values.push(new Map(), new Set(), /x/, Promise.resolve(1), new Date(0), new WeakMap(), new WeakSet()); +values.push(new Dog("rex"), new Cat("tom"), new Animal("any"), {}, [], null); +for (const name of Object.keys(classes)) { + const C = classes[name]; + console.log(name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); +} +const holder = { type: Dog }; +console.log(new Map() instanceof holder.type, new Dog("a") instanceof holder.type); diff --git a/tests/corpus/6925-instanceof-dynamic-constructors.ts b/tests/corpus/6925-instanceof-dynamic-constructors.ts new file mode 100644 index 000000000..0a39b92b2 --- /dev/null +++ b/tests/corpus/6925-instanceof-dynamic-constructors.ts @@ -0,0 +1,64 @@ +// `instanceof` in TypeScript against right-hand sides that are not class names: +// constructor-typed parameters, constructors read from tables as any, builtin +// constructors held as values, and class values held in properties. +class Animal { + name: string; + constructor(name: string) { this.name = name; } +} +class Dog extends Animal { + bark(): string { return this.name + " barks"; } +} +class Plain { v = 1; } + +// Constructor-typed parameters. +function isA(value: unknown, C: new (...args: never[]) => unknown): boolean { + return value instanceof C; +} +const dog = new Dog("rex"); +console.log(isA(dog, Animal), isA(dog, Dog), isA(new Animal("a"), Dog), isA(new Plain(), Animal), isA(dog, Plain)); +console.log(isA(new Date(0), Date), isA(new Map(), Map), isA({}, Map), isA("text", Animal), isA(null, Animal)); + +// Narrowing through a constructor-typed value. +function describe(value: Animal, C: typeof Dog): string { + if (value instanceof C) return value.bark(); + return value.name; +} +console.log(describe(dog, Dog), describe(new Animal("cat"), Dog)); + +// A table of constructors read as any. +const table: Record = { Date, Map, Set, RegExp, Promise, WeakMap, WeakSet, Dog }; +const samples: unknown[] = [new Date(0), new Map(), new Set(), /x/, Promise.resolve(1), new WeakMap(), new WeakSet(), [1], dog, { a: 1 }]; +for (const name of Object.keys(table)) { + const C = table[name]; + console.log(name, samples.map((s) => (s instanceof C ? 1 : 0)).join("")); +} + +// A right-hand side that is not callable throws Node's TypeError. +const notCallable: any = {}; +try { + console.log(dog instanceof notCallable); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +// Typed native values passed as unknown, against builtin constructors held as any. +const typedValues: [string, unknown][] = [ + ["map", new Map([["a", 1]])], + ["set", new Set([1])], + ["regex", /a/g], + ["promise", Promise.resolve(1)], + ["weak", new WeakMap()], + ["date", new Date(5)], + ["array", [1]], + ["record", { a: 1 }], +]; +const builtins: Record = { Map, Set, RegExp, Promise, WeakMap, Date }; +for (const [label, value] of typedValues) { + console.log(label, Object.keys(builtins).map((name) => (value instanceof builtins[name] ? 1 : 0)).join("")); +} + +// Class values held in properties, against native values and instances. +const holder = { type: Dog, base: Animal }; +const map = new Map(); +const pattern = /x/; +console.log(map instanceof holder.type, pattern instanceof holder.base, dog instanceof holder.type, dog instanceof holder.base); diff --git a/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6474582a0 --- /dev/null +++ b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2000 } diff --git a/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..53f54c1dd --- /dev/null +++ b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 3810, "san": 5921 } diff --git a/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..8de22d657 --- /dev/null +++ b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1 @@ +{ "plain": 3033, "san": 4212 } diff --git a/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..e2d372e0c --- /dev/null +++ b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3097, "san": 4707 } From fbaa71a66f0a6510f4a4ae0594004c1343cd803d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:54:17 -0500 Subject: [PATCH 070/169] Run the fromEntries aliasing program and drop compiled Iterable entries from the iterator fences Since Iterable values compile (#1875), two diagnostics fixtures changed: - fromentries-aliased-entries.ts compiles and prints what Node prints: the entries' one array stays shared by both fields. It moves to the run corpus as 7440-fromentries-aliased-entries.ts. - iterator-slot-fences.ts no longer reports its Iterable parameter (SC2020) and the call through it (SC2004). That part leaves the fixture, which keeps the class-iterator fence; corpus 7380-7383 cover Iterable values. --- .../7440-fromentries-aliased-entries.ts.json | 6 ++++++ .../fromentries-aliased-entries.ts.json | 6 ------ .../7440-fromentries-aliased-entries.ts | 12 +++++++++++ .../fromentries-aliased-entries.ts | 13 ------------ tests/diagnostics/iterator-slot-fences.ts | 11 ++-------- .../fromentries-aliased-entries.ts.txt | 8 -------- .../__snapshots__/iterator-slot-fences.ts.txt | 20 +------------------ .../7440-fromentries-aliased-entries.ts.json | 1 + 8 files changed, 22 insertions(+), 55 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json create mode 100644 tests/corpus/7440-fromentries-aliased-entries.ts delete mode 100644 tests/diagnostics/fromentries-aliased-entries.ts delete mode 100644 tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt create mode 100644 tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json new file mode 100644 index 000000000..108193bac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7440-fromentries-aliased-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7440-fromentries-aliased-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json deleted file mode 100644 index 7388f2b71..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/fromentries-aliased-entries.ts" - ], - "diags": [] -} diff --git a/tests/corpus/7440-fromentries-aliased-entries.ts b/tests/corpus/7440-fromentries-aliased-entries.ts new file mode 100644 index 000000000..f7c4c7e21 --- /dev/null +++ b/tests/corpus/7440-fromentries-aliased-entries.ts @@ -0,0 +1,12 @@ +// `Object.fromEntries(...) as T` over entries whose values are native +// arrays keeps their identity: both fields hold the one array the entries +// name, so a push through one is seen through the other and the original +// (corpus 6467 covers the scalar and checked-dynamic cases). +type Scores = { a: number[]; b: number[] }; +const shared = [10]; +const scores = Object.fromEntries([ + ["a", shared], + ["b", shared], +]) as Scores; +scores.a.push(11); +console.log(shared.length, scores.b.length); diff --git a/tests/diagnostics/fromentries-aliased-entries.ts b/tests/diagnostics/fromentries-aliased-entries.ts deleted file mode 100644 index ee5cc5338..000000000 --- a/tests/diagnostics/fromentries-aliased-entries.ts +++ /dev/null @@ -1,13 +0,0 @@ -// `Object.fromEntries(...) as T` keeps the object only when the entries' -// values are scalars or checked-dynamic values. Native arrays and records -// in the entries would be copied, severing their identity, so the -// assertion keeps the exact-shape fence (corpus 6467 covers the cases that -// lower). -type Scores = { a: number[]; b: number[] }; -const shared = [10]; -const scores = Object.fromEntries([ - ["a", shared], - ["b", shared], -]) as Scores; -scores.a.push(11); -console.log(shared.length, scores.b.length); diff --git a/tests/diagnostics/iterator-slot-fences.ts b/tests/diagnostics/iterator-slot-fences.ts index 759929615..e82437d4d 100644 --- a/tests/diagnostics/iterator-slot-fences.ts +++ b/tests/diagnostics/iterator-slot-fences.ts @@ -1,6 +1,6 @@ // Iterator-typed slots hold generators and built-in collection iterators. -// A class implementing the iterator protocol stays a pointed fence there, -// and Iterable annotations have no lowering yet. +// A class implementing the iterator protocol stays a pointed fence there +// (Iterable values: corpus 7380-7383). class Countdown implements Iterator { n = 2; @@ -12,10 +12,3 @@ function firstOf(it: Iterator): void { console.log(it.next().value); } firstOf(new Countdown()); - -function total(values: Iterable): number { - let sum = 0; - for (const v of values) sum += v; - return sum; -} -console.log(total([1, 2])); diff --git a/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt b/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt deleted file mode 100644 index 7b0f02266..000000000 --- a/tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt +++ /dev/null @@ -1,8 +0,0 @@ -fromentries-aliased-entries.ts:8:16 - error SC2002: record shapes must match exactly or width-coerce: expected '{ a: number[]; b: number[] }', got '{ [key: string]: number[] }' — 'a' is not a declared field of the source, and the source's index signature could hold it at runtime (a completed undefined would drop that value) - - 7 | const shared = [10]; - 8 | const scores = Object.fromEntries([ - | ^~~~~~~~~~~~~~~~~~~~ - 9 | ["a", shared], - - hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead \ No newline at end of file diff --git a/tests/harness/__snapshots__/iterator-slot-fences.ts.txt b/tests/harness/__snapshots__/iterator-slot-fences.ts.txt index fd244e8fd..a29b4b186 100644 --- a/tests/harness/__snapshots__/iterator-slot-fences.ts.txt +++ b/tests/harness/__snapshots__/iterator-slot-fences.ts.txt @@ -3,22 +3,4 @@ iterator-slot-fences.ts:14:9 - error SC1090: 'Countdown' values in an iterator-t 13 | } 14 | firstOf(new Countdown()); | ^~~~~~~~~~~~~~~ - 15 | - -iterator-slot-fences.ts:16:16 - error SC2020: 'Iterable' is part of the standard library types but has no scriptc lowering yet - - 15 | - 16 | function total(values: Iterable): number { - | ^~~~~~ - 17 | let sum = 0; - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -iterator-slot-fences.ts:21:13 - error SC2004: uses of 'total' inherit the blocker on its declaration - - 20 | } - 21 | console.log(total([1, 2])); - | ^~~~~ - 22 | - - hint: the declaration of 'total' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file + 15 | \ No newline at end of file diff --git a/tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json b/tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7440-fromentries-aliased-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } From 56149fde0433fb05751f1e8f29d03029c852f1c8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:56:53 -0500 Subject: [PATCH 071/169] Count each epoll test deadline from its own arm call The poller test recorded every initial deadline as the same start time plus the delay, while the poller reads the clock at each arm call; when arming the 2,000 timers took over 1 ms, the recorded order could differ from the delivered one by more than the assertion's slack. Each deadline now counts from the time read just before its scrp_timer_arm call, as the re-arm loop's do, and a 3 ms pause halfway through arming keeps the test exercising that case. --- packages/runtime/src/scr_loop_epoll.test.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_loop_epoll.test.c b/packages/runtime/src/scr_loop_epoll.test.c index 4c0798e29..6ab1621c5 100644 --- a/packages/runtime/src/scr_loop_epoll.test.c +++ b/packages/runtime/src/scr_loop_epoll.test.c @@ -38,10 +38,17 @@ int main(void) { assert(p != NULL); srand(7); double start = now_ms(); + /* Each deadline counts from its own arm call: arming takes time, here + * stretched by a pause halfway, and the poller reads the clock at each + * call. */ for (int i = 0; i < KEYS; i++) { + if (i == KEYS / 2) + for (double until = now_ms() + 3; now_ms() < until;) { + } double ms = 5 + rand() % 120; + double t = now_ms(); assert(scrp_timer_arm(p, &keys[i], ms, &keys[i])); - deadline[i] = start + ms; + deadline[i] = t + ms; state[i] = 1; } /* re-arm a third later and earlier, cancel a third (the heap and the key From d0334f93f5f4fc200b0793fd4b7f828426af92ed Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 17:57:59 -0700 Subject: [PATCH 072/169] Record the preflight/order baseline for the zod3-infer npm case Co-Authored-By: Claude Opus 5.5 --- .../tests/fixtures/npm/cases/zod3-infer/main.ts.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json new file mode 100644 index 000000000..80cbb3523 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-infer/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-infer/main.ts" + ], + "diags": [] +} From 33b22c5bac7ac6f7120e06792981a2233232067f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:07:43 -0500 Subject: [PATCH 073/169] Fix the native compiler build on main --- .../src/frontend/lowering/lower-stream.ts | 6 ++++-- packages/compiler/src/ir/char-unit-locals.ts | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 8309d69a9..e35f5564f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -4637,10 +4637,12 @@ function lowerOperatorCallback( "the callback's parameter and result types must cross the checked-dynamic boundary", ); } + const fn: IrExpr = { kind: "dynFrom", value: v, type: DYN, loc }; // flatMap over a compiled generator: the runtime steps it through the // helpers over its type - const gen = member === "flatMap" && v.type.ret.kind === "generator" ? v.type.ret : undefined; - return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions, ...(gen && { gen }) }; + if (member === "flatMap" && v.type.ret.kind === "generator") + return { fn, takesOptions, gen: v.type.ret }; + return { fn, takesOptions }; } /** The operator callbacks' `{ signal }` (operators.js): map and the diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index 3a5c7cd42..4008f4e1e 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -1,4 +1,12 @@ -import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { + BOOL, + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; import { mapExprChildren, mapStmtChildren } from "./traverse.js"; type CharAt = IrExpr & { kind: "strIntrinsic" }; @@ -140,10 +148,15 @@ function rewriteFunction(fn: IrFunction): IrFunction { return { ...fn, body: fn.body.map(rewriteStmt), - locals: fn.locals.map((l) => (held.has(l.id) ? { ...l, type: F64 } : l)), + locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)), }; } +/** The local, holding the code unit instead of the one-character string. */ +function unitLocal(local: IrLocal): IrLocal { + return { ...local, type: F64 }; +} + /** The local a node mentions in a form the rewrite handles: the * declaration from a one-argument charAt, a one-unit test, or a switch * over one-unit cases. */ From 122c98cf4fdd5e8c6ab5c81775a284ef009b7bda Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:48:22 -0700 Subject: [PATCH 074/169] Read the global object without rebuilding its name table Every evaluation of `globalThis` as a value built an array literal of every name the global type declares (hundreds of strings) and registered each one in the known-name table again. A server that reads a singleton through `globalThis` per request spent about a tenth of its time there. Emit the name table as one immortal static array per program (the tagged-template site array, keyed by its contents), and return the global object at once when the runtime sees a table it has already registered. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lower-global-value.ts | 9 ++++++--- packages/runtime/src/scr_node_builtin.c | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts index 4b8e7b10a..e7ab6b0f0 100644 --- a/packages/compiler/src/frontend/lowering/lower-global-value.ts +++ b/packages/compiler/src/frontend/lowering/lower-global-value.ts @@ -1,6 +1,5 @@ import * as ts from "../ts7/adapter.js"; import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js"; -import { strLit } from "../../ir/build.js"; import { locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import { TEMPORAL_GLOBAL } from "../builtin-modules.js"; @@ -19,10 +18,14 @@ export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr return { kind: "libCall", fn: "global.native", + // One immortal name table per program (keyed by its contents): reading + // the global object in a hot path allocates nothing, and the runtime + // recognizes a table it has already registered. args: [ { - kind: "arrayLit", - elems: names.map((name) => strLit(name, loc)), + kind: "templateStrings", + key: `%globalThis.names:${names.join(",")}`, + cooked: names, type: arrayOf(STRING), loc, }, diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 233617092..75df7e892 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -246,8 +246,13 @@ ScrDyn *scr_global_object(void) { } ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { + /* The compiler passes one immortal name table per program: once it is + * registered, later reads of the global object only take a reference. */ + static SCR_TL const ScrArr *registered; + if (known == registered && scr_global_box) return scr_dyn_retain(scr_global_box); ScrDyn *global = scr_global_object(); scr_global_console_get = console_get; + if (known->rc == SIZE_MAX) registered = known; for (size_t i = 0; i < known->len; i++) { ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i); if (!scr_dyn_obj_get(scr_global_known, key->data, key->len)) From 982572c173d8f6c2fb8b75512b46ee9a1fb6601a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 17:47:20 -0700 Subject: [PATCH 075/169] Dispatch splice on arrays of unknown elements Arrays the checker types with unknown elements are stored checked-dynamic, and their methods dispatch on the runtime array. splice was missing from that dispatch table, so unknown[] receivers refused it, including generic bodies instantiated at unknown (pool.splice(i, 1) in a sample helper gets such an instantiation once a program has a dynamic tier). The runtime's dynInvoke already implements splice; the table now names it, and arrays of unknown elements take the dispatch from the array-method lowering. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/containers/array-methods.ts | 15 +++++++++++++++ .../compiler/src/frontend/lowering/lower-calls.ts | 1 + .../corpus/7402-unknown-array-methods.ts.json | 6 ++++++ tests/corpus/7402-unknown-array-methods.ts | 14 ++++++++++++++ .../7402-unknown-array-methods.ts.json | 1 + 5 files changed, 37 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json create mode 100644 tests/corpus/7402-unknown-array-methods.ts create mode 100644 tests/harness/corpus-costs/7402-unknown-array-methods.ts.json diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda..3ab2e84e2 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -361,6 +361,21 @@ export function lowerArrayMethodCall( if (probe.type.kind === "array") { receiverIr = probe.type; probedUntyped = true; + } else if ( + probe.type.kind === "dyn" && + !isJsSourceFile(ts.getSourceFileOfNode(access)) && + lowerer.isStdlibMember(access) + ) { + // An `unknown[]` (or `any[]`) array stored checked-dynamic — a + // generic body instantiated at `unknown`, a spread of unknown + // elements: the method runs on the runtime array (dynInvoke, JS + // exact), and a typed result is checked like any member read. + const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, probe, true); + if (!dispatched) return null; + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + return result && result.kind !== "void" && result.kind !== "dyn" + ? lowerer.coerceInto(call, dispatched, result) + : dispatched; } } if (receiverIr?.kind === "union") { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c0406e77b..246958d54 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4243,6 +4243,7 @@ const DYN_DISPATCH_METHODS = new Set([ "shift", "unshift", "slice", + "splice", "at", "indexOf", "lastIndexOf", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json new file mode 100644 index 000000000..db33da675 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7402-unknown-array-methods.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7402-unknown-array-methods.ts b/tests/corpus/7402-unknown-array-methods.ts new file mode 100644 index 000000000..e374dd40f --- /dev/null +++ b/tests/corpus/7402-unknown-array-methods.ts @@ -0,0 +1,14 @@ +// Methods on arrays of unknown elements, which are stored checked-dynamic: +// direct `unknown[]` bindings and generic bodies instantiated at `unknown`. +const values: unknown[] = [1, "two", 3, { four: 4 }]; +const removed = values.splice(1, 2); +console.log(values, removed, removed.length); +values.splice(1, 0, "inserted", null); +console.log(values, values.indexOf(null), values.includes("inserted")); +console.log(values.slice(1).reverse(), values.join("|")); + +function takeAt(items: readonly T[], index: number): T[] { + const pool = [...items]; + return pool.splice(index, 1); +} +console.log(takeAt([10, 20, 30], 1), takeAt([true, "x", 7], 2)); diff --git a/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json new file mode 100644 index 000000000..9eb118813 --- /dev/null +++ b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20000 } From a9ee38611ebd4fa7fdd0c1638b498119270c4942 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:20:56 -0500 Subject: [PATCH 076/169] Register the global object's names again after a library re-init --- packages/runtime/src/scr_node_builtin.c | 10 +++++++--- tests/harness/library-mode.test.ts | 1 + tests/library-mode/reinit/lib.ts | 18 ++++++++++++++++++ tests/library-mode/reinit/probe.c | 3 +++ tests/library-mode/reinit/profile.json | 3 ++- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 75df7e892..c0031e67b 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -90,6 +90,10 @@ static SCR_TL ScrDyn *scr_global_own; * live on the box (string-keyed ones in scr_global_own), so every spelling * of the global object answers this box. */ static SCR_TL ScrDyn *scr_global_box; +/* The compiler's immortal name table (one per program) whose names + * scr_global_known holds; cleared with the tables, so a library re-init + * registers the names again. */ +static SCR_TL const ScrArr *scr_global_registered; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); @@ -190,6 +194,7 @@ static void scr_global_cleanup(void) { scr_dyn_release(scr_global_own); scr_global_known = NULL; scr_global_own = NULL; + scr_global_registered = NULL; } static void *scr_global_retain(void *handle) { return handle; } @@ -248,11 +253,10 @@ ScrDyn *scr_global_object(void) { ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { /* The compiler passes one immortal name table per program: once it is * registered, later reads of the global object only take a reference. */ - static SCR_TL const ScrArr *registered; - if (known == registered && scr_global_box) return scr_dyn_retain(scr_global_box); + if (known == scr_global_registered && scr_global_box) return scr_dyn_retain(scr_global_box); ScrDyn *global = scr_global_object(); scr_global_console_get = console_get; - if (known->rc == SIZE_MAX) registered = known; + if (known->rc == SIZE_MAX) scr_global_registered = known; for (size_t i = 0; i < known->len; i++) { ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i); if (!scr_dyn_obj_get(scr_global_known, key->data, key->len)) diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index ffd42bd8e..0d4052952 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -351,6 +351,7 @@ bump: 1 2 note: 1 2 indexed: 72100 recall: a,b +globals: undefined fenced `; test("K4: three init sessions are byte-identical", async () => { diff --git a/tests/library-mode/reinit/lib.ts b/tests/library-mode/reinit/lib.ts index dad54090a..73e466f70 100644 --- a/tests/library-mode/reinit/lib.ts +++ b/tests/library-mode/reinit/lib.ts @@ -29,4 +29,22 @@ export function indexedUnicode(): number { return s.length + s.charCodeAt(n) + s.slice(n - 1, n + 2).length; } +// A symbol-keyed global access creates the global object before any read of +// `globalThis` in a session: the builtin names must still be fenced on the +// stored global object, in every session. +const probeKey = Symbol.for("scriptc.reinit"); +function stored(global: typeof globalThis): any { + return global; +} +export function globalFences(): string { + // @ts-expect-error: globalThis has no symbol index signature + const before: unknown = globalThis[probeKey]; + const view = stored(globalThis); + try { + return `${typeof before} ${typeof view.Reflect}`; + } catch { + return `${typeof before} fenced`; + } +} + console.log(`session start counter=${counter}`); diff --git a/tests/library-mode/reinit/probe.c b/tests/library-mode/reinit/probe.c index 8cd17be1e..409766d0d 100644 --- a/tests/library-mode/reinit/probe.c +++ b/tests/library-mode/reinit/probe.c @@ -12,6 +12,7 @@ extern double kr_bump(void); extern double kr_note(const uint8_t *p, size_t len); extern double kr_indexed_unicode(void); extern void kr_recall(const uint8_t **out, size_t *out_len); +extern void kr_global_fences(const uint8_t **out, size_t *out_len); static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { (void)ctx; (void)addr; @@ -30,6 +31,8 @@ static void session(void) { const uint8_t *s; size_t n; kr_recall(&s, &n); printf("recall: %.*s\n", (int)n, s); + kr_global_fences(&s, &n); + printf("globals: %.*s\n", (int)n, s); } int main(void) { diff --git a/tests/library-mode/reinit/profile.json b/tests/library-mode/reinit/profile.json index 620150db1..cfcfebc9a 100644 --- a/tests/library-mode/reinit/profile.json +++ b/tests/library-mode/reinit/profile.json @@ -14,6 +14,7 @@ { "export": "bump", "symbol": "kr_bump", "params": [], "returns": "f64" }, { "export": "note", "symbol": "kr_note", "params": ["string"], "returns": "f64" }, { "export": "recall", "symbol": "kr_recall", "params": [], "returns": "string" }, - { "export": "indexedUnicode", "symbol": "kr_indexed_unicode", "params": [], "returns": "f64" } + { "export": "indexedUnicode", "symbol": "kr_indexed_unicode", "params": [], "returns": "f64" }, + { "export": "globalFences", "symbol": "kr_global_fences", "params": [], "returns": "string" } ] } From 5966f23bd369bfb916db8175d53b0170eab6743f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:33:33 -0500 Subject: [PATCH 077/169] Keep the class-value call fence when a subclass declares a static call `C.call(...)` through a class value typed as `Base` may run a subclass's own static `call`, which Node calls instead of the class constructor. The constructor-call lowering now applies only when no class the receiver can hold declares a static `call` or `apply`: the class and its bases (any static member of that name, including accessors, fields without initializers and computed names), and, for a receiver that may hold a subclass, every subclass. Otherwise the call keeps its existing diagnostic. --- .../src/frontend/lowering/lower-classes.ts | 42 ++++++++++++++++++- .../class-call-shadowed-static.ts.json | 6 +++ .../diagnostics/class-call-shadowed-static.ts | 15 +++++++ .../class-call-shadowed-static.ts.txt | 8 ++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json create mode 100644 tests/diagnostics/class-call-shadowed-static.ts create mode 100644 tests/harness/__snapshots__/class-call-shadowed-static.ts.txt diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 17d48913a..a3eddf09a 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -6738,8 +6738,10 @@ function staticCallOn( * evaluated (and, for apply, after the argument list is read). The boxed * class value's call path throws exactly that. Neither expando statics nor * writes through untyped values can give a class an own `call` or `apply`. - * The receiver must be one the caller does not evaluate again (a name, - * `this`, or a namespace member). Null otherwise. */ + * A receiver that may hold a subclass at run time (a class value, `this` in + * a static method) needs every subclass to lack such a static too. The + * receiver must be one the caller does not evaluate again (a name, `this`, + * or a namespace member). Null otherwise. */ function classConstructorCall( lowerer: Lowerer, call: ts.CallExpression, @@ -6757,6 +6759,9 @@ function classConstructorCall( info.builtinStream || info.builtinEventTarget || info.runtimeStatics?.length || + declaresStaticAbove(info, name) || + (exactClassOfReceiver(lowerer, receiver) !== info && + info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))) || call.arguments.nodes.some(ts.isSpreadElement) || !( ts.isIdentifier(receiver) || @@ -6776,6 +6781,39 @@ function classConstructorCall( }; } +/** Whether `info`'s declaration has a static member that may be named `name` + * (a method, field or accessor of that name, or a computed name), or `info` + * has no declaration to tell. */ +function declaresStatic(info: ClassInfo, name: string): boolean { + if (!info.decl) return true; + return info.decl.members.nodes.some( + (member) => + member.name !== undefined && + ts.canHaveModifiers(member) && + ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) === + true && + (ts.isComputedPropertyName(member.name) || + ((ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) && + member.name.text === name)), + ); +} + +/** declaresStatic over `info` and its bases, which it inherits statics from. */ +function declaresStaticAbove(info: ClassInfo, name: string): boolean { + for (let c: ClassInfo | null = info; c; c = c.base) if (declaresStatic(c, name)) return true; + return false; +} + +/** declaresStatic over `info` and its subclasses, or runtime statics, which a + * function-local JavaScript class defines at run time. */ +function declaresStaticBelow(info: ClassInfo, name: string): boolean { + return ( + declaresStatic(info, name) || + !!info.runtimeStatics?.length || + info.subclasses.some((subclass) => declaresStaticBelow(subclass, name)) + ); +} + /** `C.m(...)` where `m` is a static method that a function-local * JavaScript class defines at runtime (a factory's `class extends base`), * inherited by `C` through its statically known base chain. When the body diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json new file mode 100644 index 000000000..6c87e5f66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/class-call-shadowed-static.ts" + ], + "diags": [] +} diff --git a/tests/diagnostics/class-call-shadowed-static.ts b/tests/diagnostics/class-call-shadowed-static.ts new file mode 100644 index 000000000..53a5dc9ad --- /dev/null +++ b/tests/diagnostics/class-call-shadowed-static.ts @@ -0,0 +1,15 @@ +// Through a class value that may hold a subclass, `C.call(...)` reaches the +// subclass's own static `call` when it declares one, so the call is not the +// constructor call that throws Node's TypeError. +class Base { + n = 1; +} +class OwnCall extends Base { + static call(value: unknown): string { + return `own call ${typeof value}`; + } +} +function callThrough(C: typeof Base): unknown { + return C.call(new Base()); +} +console.log(callThrough(OwnCall)); diff --git a/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt b/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt new file mode 100644 index 000000000..e04c8423a --- /dev/null +++ b/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt @@ -0,0 +1,8 @@ +class-call-shadowed-static.ts:13:10 - error SC2020: 'typeof Base.call' is part of the standard library types but has no scriptc lowering yet + + 12 | function callThrough(C: typeof Base): unknown { + 13 | return C.call(new Base()); + | ^~~~~~ + 14 | } + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file From 3991a49094d3fdc5a058d7383efe26dea5483b58 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:40:56 -0500 Subject: [PATCH 078/169] Hold any later value in JavaScript var bindings A JavaScript `var` took the checker's type of its initializer, which no later write is checked against in a JavaScript file. js-yaml 4.1.1 failed on every timestamp with "a 'string' value is not representable in the target union": its `var fraction = 0` holds the fraction's digits as a string while it pads them (`fraction = match[7].slice(0, 3)`). Every YAML error also threw the "assignment to non-variables" fence instead of the YAMLException, because its error mark is a `var` object literal that gains a member (`mark.snippet = snippet(mark)`). - A `var` whose writes the declared type cannot hold is checked-dynamic, by the rules `let` already uses (jsBindingHasOpenWrites), and so is a `var` whose object literal gains members or carries methods (jsDeclarationExpandsObject, shared with `let` and `const`). - `+=` of a string onto a number or boolean binding (`width += "px"`) counts as such a write, for `var` and `let`. - A JavaScript function that returns such a binding returns a checked-dynamic value (an async one a promise of one) instead of the initializer's type. A string binding that is only appended to stays a string. --- .../src/frontend/lowering/function-returns.ts | 44 ++++++ .../src/frontend/lowering/lower-stmts.ts | 133 +++++++++++++----- .../7037-js-var-open-writes/main.ts.json | 6 + .../corpus/7038-js-var-later-writes.mjs.json | 6 + tests/corpus/7037-js-var-open-writes/main.ts | 18 +++ .../node_modules/scalar-marks/index.d.ts | 4 + .../node_modules/scalar-marks/index.js | 81 +++++++++++ .../node_modules/scalar-marks/package.json | 1 + tests/corpus/7038-js-var-later-writes.mjs | 59 ++++++++ .../7037-js-var-open-writes/main.ts.json | 1 + .../7038-js-var-later-writes.mjs.json | 1 + 11 files changed, 321 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json create mode 100644 tests/corpus/7037-js-var-open-writes/main.ts create mode 100644 tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts create mode 100644 tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js create mode 100644 tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json create mode 100644 tests/corpus/7038-js-var-later-writes.mjs create mode 100644 tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json create mode 100644 tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 5ede3efe3..dde109ea7 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -15,6 +15,7 @@ import { import { isJsSourceFile } from "../program.js"; import { PoisonError, dynFallbackType } from "./lowerer.js"; import { functionCanReturnUndefined } from "../function-completion.js"; +import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js"; import type { FnCtx } from "./function-context.js"; /** Keep the conservative completion rule shared by return construction @@ -124,6 +125,32 @@ export function producesConstructor(lowerer: Lowerer, type: ts.Type): boolean { return false; } +/** Whether a return of `decl` itself (not of a nested function) is a bare + * read of a JavaScript variable whose writes the checker does not + * constrain. A string that is only appended to stays a string. */ +function returnsOpenJsBinding(lowerer: Lowerer, decl: ts.SignatureDeclaration): boolean { + let open = false; + const opens = (value: ts.Expression | undefined): boolean => { + while (value && ts.isParenthesizedExpression(value)) value = value.expression; + if (!value || !ts.isIdentifier(value)) return false; + const binding = lowerer.resolveValueSymbol(value)?.valueDeclaration; + return ( + binding !== undefined && + ts.isVariableDeclaration(binding) && + jsBindingHasOpenWrites(lowerer, binding) && + !jsBindingAppendsOnly(lowerer, binding) + ); + }; + const scan = (node: ts.Node): void => { + if (open || (node !== decl && ts.isFunctionLike(node))) return; + if (ts.isReturnStatement(node) && opens(node.expression)) open = true; + ts.forEachChild(node, scan); + }; + if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) open = opens(decl.body); + else scan(decl); + return open; +} + /** Map a declaration's return contract while retaining JavaScript * inference fallbacks. Diagnostics preserve the caller's blame node. */ export function declaredReturnType( @@ -212,6 +239,23 @@ export function declaredReturnType( lowerer.unions.get(mappedReturn.unionId)?.arms.some((arm) => arm.kind === "func") ) return DYN; + // A JS function returning a local that holds whatever its writes store + // (jsBindingHasOpenWrites): the inferred result is the initializer's + // type, which the returned value need not have. + // An async function's promise fulfills with that value. + if ( + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !decl.type && + !decl.asteriskToken && + !hasExplicitJsDocReturn(decl) && + mappedReturn !== null && + mappedReturn.kind !== "dyn" && + !(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") && + returnsOpenJsBinding(lowerer, decl) + ) + return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ? { kind: "promise", inner: DYN } + : DYN; // JS inference assigns Number to arithmetic over any, although the // runtime operands can both be BigInts. Preserve that native result. if ( diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ea5ef72ac..62c2e3a0f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1220,6 +1220,43 @@ function lowerJsVarFromExisting( }; } +/** Whether a JavaScript declaration's object value changes shape after its + * initializer (jsOpenObjectType, jsObjectBindingExpands) or carries + * methods, so only the checked-dynamic representation holds it. + * `inferred` is the declaration's mapped checker type. */ +function jsDeclarationExpandsObject( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + inferred: IrType | null, +): boolean { + const arms = + inferred?.kind === "union" + ? (lowerer.unions.get(inferred.unionId)?.arms ?? []) + : inferred + ? [inferred] + : []; + return ( + (!decl.type && + !hasJsTypeAnnotation(decl) && + inferred !== null && + jsOpenObjectType(decl, inferred, lowerer.shapes, lowerer.unions).kind === "dyn") || + arms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)) || + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !decl.type && + !hasJsTypeAnnotation(decl) && + decl.initializer !== undefined && + ts.isObjectLiteralExpression(decl.initializer) && + decl.initializer.properties.nodes.some( + (property) => + ts.isMethodDeclaration(property) || + ts.isAccessor(property) || + (ts.isPropertyAssignment(property) && + (ts.isFunctionExpression(property.initializer) || + ts.isArrowFunction(property.initializer))), + )) + ); +} + /** The `var` symbol's function-scoped binding TYPE — the declared type at * the binding name, with the JS-source fallbacks every mutable binding * takes (an empty-object-literal type is checked-dynamic because tsc @@ -1254,6 +1291,19 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul type = staticImportNamespaceType(lowerer, nameNode.parent.initializer) ?? type; } if (!type || type.kind === "void") return null; + // A JavaScript `var` holds whatever its writes store, as a `let` does: + // the checker types it by its initializer and checks no later write + // (`var fraction = 0;`, later `fraction = match[7].slice(0, 3)`), and an + // object it holds may gain members (`mark.snippet = snippet(mark)`). + if ( + type.kind !== "dyn" && + ts.isVariableDeclaration(nameNode.parent) && + nameNode.parent.name === nameNode && + isJsSourceFile(ts.getSourceFileOfNode(nameNode)) && + (jsBindingHasOpenWrites(lowerer, nameNode.parent) || + jsDeclarationExpandsObject(lowerer, nameNode.parent, type)) + ) + return DYN; // A binding readable before its first store holds undefined until then. return lowerer.runtimeOptionalBindingType(nameNode, type); } @@ -5597,26 +5647,7 @@ export function lowerVarDecl( : inferredObjectType ? [inferredObjectType] : []; - const expandsObject = - (!decl.type && - !hasJsTypeAnnotation(decl) && - inferredObjectType !== null && - jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn") || - inferredObjectArms.some( - (arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm), - ) || - (isJsSourceFile(ts.getSourceFileOfNode(decl)) && - !decl.type && - !hasJsTypeAnnotation(decl) && - ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.nodes.some( - (property) => - ts.isMethodDeclaration(property) || - ts.isAccessor(property) || - (ts.isPropertyAssignment(property) && - (ts.isFunctionExpression(property.initializer) || - ts.isArrowFunction(property.initializer))), - )); + const expandsObject = jsDeclarationExpandsObject(lowerer, decl, inferredObjectType); let init: IrExpr; try { init = @@ -6327,18 +6358,15 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean { ); } -/** Inferred JS scalar initializers do not constrain later writes through - * an any-valued callback. Keep those bindings open, including captured writes. */ -export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { - if ( - !isJsSourceFile(ts.getSourceFileOfNode(decl)) || - decl.type || - hasJsTypeAnnotation(decl) || - !ts.isIdentifier(decl.name) - ) - return false; - const symbol = lowerer.resolveValueSymbol(decl.name); - if (!symbol) return false; +/** The assignments to a declaration's binding in its function (or file), + * `=` and compound alike. Null when the name has no symbol. */ +function jsBindingWrites( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + name: ts.Identifier, +): ts.BinaryExpression[] | null { + const symbol = lowerer.resolveValueSymbol(name); + if (!symbol) return null; let owner: ts.Node = decl; while (owner.parent && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner)) owner = owner.parent; let writes = lowerer.jsBindingWritesByOwner.get(owner); @@ -6359,9 +6387,36 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar writes = indexed; lowerer.jsBindingWritesByOwner.set(owner, writes); } - const candidates = (writes.get(decl.name.text) ?? []).filter( + return (writes.get(name.text) ?? []).filter( (node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol, ); +} + +/** Whether a JavaScript binding always holds a string: the checker types it + * as one, and every write appends to it (`result += part`), which yields a + * string whatever the part is. */ +export function jsBindingAppendsOnly(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (!ts.isIdentifier(decl.name)) return false; + if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "string") return false; + const writes = jsBindingWrites(lowerer, decl, decl.name); + return ( + writes !== null && + writes.every((node) => node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) + ); +} + +/** Inferred JS scalar initializers do not constrain later writes through + * an any-valued callback. Keep those bindings open, including captured writes. */ +export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if ( + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || + decl.type || + hasJsTypeAnnotation(decl) || + !ts.isIdentifier(decl.name) + ) + return false; + const candidates = jsBindingWrites(lowerer, decl, decl.name); + if (candidates === null) return false; if ( candidates.some( (node) => @@ -6369,6 +6424,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar ) ) return true; + // A `+=` of a string turns a number or boolean binding into a string + // (`n += "px"`); the checker keeps the declared type. + if ( + candidates.some( + (node) => + node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken && + (lowerer.typeOf(node.right).flags & ts.TypeFlags.StringLike) !== 0, + ) + ) { + const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if (declared?.kind === "f64" || declared?.kind === "bool") return true; + } // A write that can store null or undefined into a binding the checker // types as a primitive (`offset = match[3] || null`): JavaScript files // are checked without strict null checks, so the declared type does not diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json new file mode 100644 index 000000000..a1bdb91b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7037-js-var-open-writes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json new file mode 100644 index 000000000..1e51a088f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7038-js-var-later-writes.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7037-js-var-open-writes/main.ts b/tests/corpus/7037-js-var-open-writes/main.ts new file mode 100644 index 000000000..cc0612974 --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/main.ts @@ -0,0 +1,18 @@ +// A JavaScript package whose `var` bindings start as one type and later +// hold another, as js-yaml's timestamp and error code does: a number that +// becomes a string and back (`fraction = match[7].slice(0, 3)`), an object +// literal that gains a member (`mark.snippet = snippet(mark)`), and `+=` of +// a string onto a number. Node runs every write; each binding holds it. +import { errorAt, millis, label, total } from "scalar-marks"; + +console.log(millis("2001-12-14t21:59:43.10"), millis("2001-12-14t21:59:43"), millis("1970-01-01t00:00:00.12345")); +for (const [input, at] of [["a: [1", 5], ["key: 'open", 10], ["a:\n [1", 7], ["ok", 1]] as const) { + try { + console.log(errorAt(input, at, "unexpected end")); + } catch (e) { + const err = e as { name: string; message: string; mark: { line: number; column: number; snippet: string } }; + console.log(err.name, "|", err.message, "|", err.mark.line, err.mark.column, JSON.stringify(err.mark.snippet)); + } +} +console.log(label(3), label(0)); +console.log(total([1, 2, 3]), total(["a", "b"]), total([1, "b", 2])); diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts new file mode 100644 index 000000000..0e0f8252c --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts @@ -0,0 +1,4 @@ +export declare function millis(data: string): number; +export declare function errorAt(input: string, position: number, message: string): string; +export declare function label(n: number): string; +export declare function total(values: (number | string)[]): number | string; diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js new file mode 100644 index 000000000..7d6b6116c --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js @@ -0,0 +1,81 @@ +"use strict"; +var TIMESTAMP = new RegExp( + '^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)' + + '[Tt]([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?$'); + +// The milliseconds of a timestamp: the fraction starts as the number 0, +// holds its digits as a string while it is padded, and ends as a number. +function millis(data) { + var match, fraction = 0, delta = null; + match = TIMESTAMP.exec(data); + if (match === null) throw new Error('Date resolve error'); + if (match[7]) { + fraction = match[7].slice(0, 3); + while (fraction.length < 3) { + fraction += '0'; + } + fraction = +fraction; + } + if (delta) fraction -= delta; + return Date.UTC(+match[1], +match[2] - 1, +match[3], +match[4], +match[5], +match[6], fraction); +} + +function State(input) { + this.input = input; + this.position = 0; + this.line = 0; + this.lineStart = 0; +} + +function snippet(mark) { + var start = mark.position, text = mark.buffer; + return text.slice(0, start) + '<' + text.slice(start) + '>'; +} + +function MarkError(reason, mark) { + Error.call(this); + this.name = 'MarkError'; + this.reason = reason; + this.mark = mark; + this.message = reason + ' (' + (mark.line + 1) + ':' + (mark.column + 1) + ')'; +} +MarkError.prototype = Object.create(Error.prototype); +MarkError.prototype.constructor = MarkError; + +// The error's mark is an object literal that gains its snippet afterwards. +function generateError(state, message) { + var mark = { + buffer: state.input, + position: state.position, + line: state.line, + column: state.position - state.lineStart + }; + mark.snippet = snippet(mark); + return new MarkError(message, mark); +} + +function errorAt(input, position, message) { + var state = new State(input); + for (var i = 0; i < position && i < input.length; i++) { + state.position++; + if (input[i] === '\n') { state.line++; state.lineStart = state.position; } + } + if (state.position >= input.length) throw generateError(state, message); + return 'no error at ' + state.position; +} + +// A number binding that `+=` turns into a string. +function label(n) { + var width = 10; + width += 'px'; + return n + ' ' + width; +} + +// A sum that becomes a string concatenation once a string arrives. +function total(values) { + var sum = 0; + for (var i = 0; i < values.length; i++) sum += values[i]; + return sum; +} + +module.exports = { millis: millis, errorAt: errorAt, label: label, total: total }; diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json new file mode 100644 index 000000000..ade0a9891 --- /dev/null +++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json @@ -0,0 +1 @@ +{ "name": "scalar-marks", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/corpus/7038-js-var-later-writes.mjs b/tests/corpus/7038-js-var-later-writes.mjs new file mode 100644 index 000000000..48b85d77a --- /dev/null +++ b/tests/corpus/7038-js-var-later-writes.mjs @@ -0,0 +1,59 @@ +// JavaScript `var` and `let` bindings that later hold another type than +// their initializer's, which nothing checks in a JavaScript file: Node +// runs each write, and a function returning the binding returns what it +// holds. +function retyped(input) { + var value = 0; + value = input.slice(0, 2); + return value; +} + +function padded(digits) { + var fraction = 0; + if (digits) { + fraction = digits.slice(0, 3); + while (fraction.length < 3) fraction += "0"; + fraction = +fraction; + } + return fraction + 1; +} + +function suffixed(n) { + var width = n * 2; + width += "px"; + return width; +} + +function letRetyped(record) { + let current = 1; + current = record.name; + return current; +} + +function grown(line) { + var mark = { line: line, column: line - 1 }; + mark.text = "at " + (mark.line + 1); + return mark; +} + +async function awaitedTotal(values) { + var total = 0; + for (var i = 0; i < values.length; i++) total += await values[i]; + console.log("awaited", total, typeof total); + return total; +} + +function counted(values) { + var count = 0; + for (var i = 0; i < values.length; i++) count += values[i]; + return count; +} + +console.log(retyped("abc"), typeof retyped("abc")); +console.log(padded("5"), padded("12345"), padded("")); +console.log(suffixed(4), typeof suffixed(4)); +console.log(letRetyped({ name: "x" }), letRetyped({ name: 2 })); +console.log(JSON.stringify(grown(3)), Object.keys(grown(0)).join(",")); +console.log(counted([1, 2]), counted([1, "a", 2])); +await awaitedTotal([1, Promise.resolve(2)]); +await awaitedTotal([1, Promise.resolve("b")]); diff --git a/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json b/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json new file mode 100644 index 000000000..2e747487d --- /dev/null +++ b/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1970, "san": 2150 } diff --git a/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json b/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json new file mode 100644 index 000000000..29130736b --- /dev/null +++ b/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json @@ -0,0 +1 @@ +{ "plain": 1570, "san": 2390 } From 5e4303e62d9da01a322bbff6ac22c01537774d5d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:52:22 -0500 Subject: [PATCH 079/169] Keep the native compiler's LLVM text as the emitter's parts The LLVM emitter builds a module as parts and joined them into one string: on a large program such as tsc-ts the parts and the 150 MB joined text were live together at the end of emission. The native compiler only writes, escapes and hashes the text, so it now takes the emitter's parts (emitLlvmModuleParts) and never joins them. Consecutive small parts are joined into batches of about 1 MiB as they are written, escaped or hashed (joinedParts), so the many small parts cost few calls. A frontend cache hit reads the text back as one part. The three emission entry points share one module pipeline (moduleEmitter). --- packages/compiler/src/backend/llvm/emitter.ts | 47 +++++++------------ packages/compiler/src/native/cache.ts | 9 +++- packages/compiler/src/native/compiler.ts | 8 ++-- .../compiler/src/native/executable-cache.ts | 6 +-- packages/compiler/src/native/prepare.test.ts | 16 +++++-- packages/compiler/src/native/prepare.ts | 23 +++++---- .../compiler/src/native/text-file.test.ts | 12 ++++- packages/compiler/src/native/text-file.ts | 24 ++++++++-- 8 files changed, 89 insertions(+), 56 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..9d6bc2dfd 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -483,7 +483,8 @@ function versionParameters(mod: IrModule): IrModule { return versionLoopBoundParameters(mod, mangleFunction); } -export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { +/** The emitter for `input` after the module passes every emission runs. */ +function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. const mod = unshareNodes(input); @@ -515,7 +516,19 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) ) : mod, options, - ).emit(); + ); +} + +export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { + return moduleEmitter(input, options).emit(); +} + +/** emitLlvmModule's text as parts that concatenate to it, without the copy + * that joining them makes: a large program's text is 150 MB. */ +export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] { + return moduleEmitter(input, options) + .emitParts() + .flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); } /** Keep large translation units below the host's single-string limit. */ @@ -525,35 +538,7 @@ export function emitLlvmModuleSource( ): string | readonly string[] { // IR validation, contracts and the frontend's disposal run after lowering without checks. relieveHeapPressure(true); - const mod = unshareNodes(input); - const parts = new LlEmitter( - options.debugSources === undefined - ? versionParameters( - addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), - ), - ), - ), - ), - ), - mangleFunction, - ), - mangleFunction, - ), - ), - ), - ) - : mod, - options, - ).emitParts(); + const parts = moduleEmitter(input, options).emitParts(); const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1)); if (length <= 256 * 1024 * 1024) return parts.join("\n"); return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part])); diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index 566df308a..3c9bdf411 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -14,7 +14,7 @@ import { } from "node:fs"; import { join } from "node:path"; import { resolveBuildCacheRoot } from "../backend/cache-root.js"; -import { writeTextGeneratedSync } from "./text-file.js"; +import { joinedParts, writeTextGeneratedSync } from "./text-file.js"; /** UTF-8 bytes one native compiler string can hold (the runtime's * SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads @@ -28,6 +28,13 @@ export function contentDigest(value: string | Uint8Array): string { return hash.digest("hex"); } +/** contentDigest of the text the parts concatenate to. */ +export function partsDigest(parts: readonly string[]): string { + const hash = createHash("sha256"); + for (const part of joinedParts(parts)) hash.update(part); + return hash.digest("hex"); +} + /** contentDigest of a file's bytes, read in 1 MiB blocks so a large file * (a module's LLVM text) is never held whole. */ export function fileDigest(path: string): string { diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index 2c850cb14..a37d76f80 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -362,7 +362,7 @@ export class NativeCompiler { mkdirSync(inputDirectory); const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll"); // In slices: the module text is never encoded whole. - writeTextPartsSync(llvmInput, [llvm]); + writeTextPartsSync(llvmInput, llvm); timing("llvm-write"); if (outputKind === "llvm") { renameSync(stagedOutput, output); @@ -496,7 +496,7 @@ export class NativeCompiler { renameSync(stagedOutput + ".dSYM", output + ".dSYM"); } mkdirSync(options.outDir, { recursive: true }); - writeTextPartsSync(llvmPath, [llvm]); + writeTextPartsSync(llvmPath, llvm); let irPath: string | undefined; if (options.emitIr) { irPath = join(options.outDir, `${stem}.ir.json`); @@ -585,7 +585,7 @@ export class NativeCompiler { const llvm = prepared.llvm; const llvmInput = join(stage, "program.ll"); // In slices: the module text is never encoded whole. - writeTextPartsSync(llvmInput, [llvm]); + writeTextPartsSync(llvmInput, llvm); timing("llvm-write"); const features = prepared.features; const pack = selectNativeRuntimePack( @@ -648,7 +648,7 @@ export class NativeCompiler { }); } timing("native-archive"); - writeTextPartsSync(llvmPath, [llvm]); + writeTextPartsSync(llvmPath, llvm); let irPath: string | undefined; if (options.emitIr) { irPath = join(options.outDir, `${stem}.lib.ir.json`); diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts index a122fb759..c0ac755b9 100644 --- a/packages/compiler/src/native/executable-cache.ts +++ b/packages/compiler/src/native/executable-cache.ts @@ -11,7 +11,7 @@ import { import { runNativeTool } from "../backend/native-tools.js"; import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js"; import type { NativeToolchain } from "./toolchain.js"; -import { contentDigest, NativeCache } from "./cache.js"; +import { contentDigest, NativeCache, partsDigest } from "./cache.js"; interface InputIdentity { path: string; @@ -254,7 +254,7 @@ export class NativeExecutableCache { export function openNativeExecutableCache( cache: NativeCache | null, toolchain: NativeToolchain, - llvm: string, + llvm: readonly string[], options: CompileRequestOptions, ffi: FfiProfile | null, pack: NativeRuntimeSelection, @@ -299,7 +299,7 @@ export function openNativeExecutableCache( const key = contentDigest( JSON.stringify({ schema: 1, - llvm: contentDigest(llvm), + llvm: partsDigest(llvm), options, toolchain, runtimeIdentity: pack.packageText, diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts index 7e22450da..95274f1cd 100644 --- a/packages/compiler/src/native/prepare.test.ts +++ b/packages/compiler/src/native/prepare.test.ts @@ -11,6 +11,12 @@ import { NativeCache } from "./cache.js"; import { prepareNativeExecutable } from "./prepare.js"; import type { NativeToolchain } from "./toolchain.js"; +/** A prepared input with its LLVM parts joined: a cache hit reads the text + * back as one part. */ +function text(input: ReturnType["prepare"]>) { + return input.ok ? { ...input, llvm: input.llvm.join("") } : input; +} + const scratch: string[] = []; afterEach(() => { for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true }); @@ -68,14 +74,14 @@ test("warm frontend reuse survives output creation and invalidates imported sour if (!first.ok) return; mkdirSync(f.options.outDir); writeFileSync(f.options.outPath, "binary placeholder"); - writeFileSync(join(f.options.outDir, "main.ll"), first.llvm); + writeFileSync(join(f.options.outDir, "main.ll"), first.llvm.join("")); const warm = f.prepare(); - expect(warm).toEqual(first); + expect(text(warm)).toEqual(text(first)); expect(f.loads()).toBe(1); writeFileSync(f.dependency, 'export const value = "second";\n'); const edited = f.prepare(); expect(edited.ok).toBe(true); - if (edited.ok) expect(edited.llvm).not.toBe(first.llvm); + if (edited.ok) expect(edited.llvm.join("")).not.toBe(first.llvm.join("")); expect(f.loads()).toBe(2); }); @@ -101,13 +107,13 @@ test("serialized IR travels as bounded parts through the frontend cache", () => expect(mod.functions.length).toBeGreaterThan(0); const warm = f.prepare({ emitIr: true }); expect(f.loads()).toBe(1); - expect(warm).toEqual(first); + expect(text(warm)).toEqual(text(first)); const family = join(f.cache.root, "frontend"); const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!; const payload = JSON.parse(readFileSync(join(family, key), "utf8")); expect(payload.schema).toBe("scriptc.native-frontend.v3"); expect(payload.input.llvm).toBe(""); expect(payload.input.ir).toBeNull(); - expect(payload.llvm).toBe(first.llvm); + expect(payload.llvm).toBe(first.llvm.join("")); expect(payload.ir).toEqual(first.ir); }); diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index edc90c449..a2c627edf 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -7,7 +7,11 @@ import type { CompileLibraryOptions, CompileRequestOptions, } from "../compile-types.js"; -import { emitLlvmModule, type ForkPrimitives, lowRegionTarget } from "../backend/llvm/emitter.js"; +import { + emitLlvmModuleParts, + type ForkPrimitives, + lowRegionTarget, +} from "../backend/llvm/emitter.js"; import { inlineRcOption } from "../backend/llvm/shapes.js"; import { executableLinkFeatures } from "../backend/executable-features.js"; import type { NativeLinkFeatures } from "../backend/native-link-info-core.js"; @@ -29,13 +33,15 @@ import type { LibraryProfile } from "../library/library-profile.js"; import type { IrModule } from "../ir/ir.js"; import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js"; import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js"; -import { batchTextParts, textSlices } from "./text-file.js"; +import { batchTextParts, joinedParts, textSlices } from "./text-file.js"; import type { NativeToolchain } from "./toolchain.js"; import { nativeFileIdentity } from "./file-identity.js"; export interface NativeExecutableInput { ok: true; - llvm: string; + /** The module's LLVM text as parts that concatenate to it + * (emitLlvmModuleParts), never joined: a large program's is 150 MB. */ + llvm: readonly string[]; /** Compact serialized IR as bounded parts (`batchTextParts`): compiler-scale * modules exceed one native string. */ ir: string[] | null; @@ -71,7 +77,8 @@ function* frontendCacheParts( input: { ...input, llvm: "", ir: null }, }); yield metadata.slice(0, metadata.length - 1) + ',"llvm":"'; - for (const slice of textSlices(input.llvm)) yield JSON.stringify(slice).slice(1, -1); + for (const part of joinedParts(input.llvm)) + for (const slice of textSlices(part)) yield JSON.stringify(slice).slice(1, -1); yield '"'; if (input.ir === null) yield ',"ir":null}'; else { @@ -122,7 +129,7 @@ function readFrontendCache( const cached: unknown = JSON.parse(bytes.toString("utf8")); if (!validCachedInput(cached) || !frontendInputsStillMatch(cached.probes, exclusions)) return null; - return { ...cached.input, llvm: cached.llvm, ir: cached.ir }; + return { ...cached.input, llvm: [cached.llvm], ir: cached.ir }; } function implementationIdentity(toolchain: NativeToolchain): string { @@ -310,8 +317,8 @@ export function prepareNativeExecutable( timing("ir-serialize"); const llvm = outputKind === "ir" - ? "" - : emitLlvmModule(prepared.mod, { + ? [] + : emitLlvmModuleParts(prepared.mod, { targetTriple: toolchain.target.llvmTriple, pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", @@ -427,7 +434,7 @@ export function prepareNativeLibrary( ); }); if (!prepared.ok) return prepared; - const llvm = emitLlvmModule(prepared.mod, { + const llvm = emitLlvmModuleParts(prepared.mod, { targetTriple: toolchain.target.llvmTriple, pointerBits: toolchain.target.pointerBits, wasi: toolchain.target.platform === "wasi", diff --git a/packages/compiler/src/native/text-file.test.ts b/packages/compiler/src/native/text-file.test.ts index b497bc4a2..7534b5058 100644 --- a/packages/compiler/src/native/text-file.test.ts +++ b/packages/compiler/src/native/text-file.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; -import { textSlices, writeTextPartsSync } from "./text-file.js"; +import { joinedParts, textSlices, writeTextPartsSync } from "./text-file.js"; const directories: string[] = []; afterEach(() => { @@ -40,3 +40,13 @@ test("a part larger than one slice is written as its UTF-8 bytes", () => { expect(seen.join("")).toBe("head," + large + "tail"); expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024); }); + +test("small parts join into batches and a long part passes through whole", () => { + const small = Array.from({ length: 3000 }, (_, index) => `line ${index}\n`); + const long = "x".repeat(2 * 1024 * 1024); + const parts = [...small, long, "tail"]; + const joined = [...joinedParts(parts)]; + expect(joined.join("")).toBe(parts.join("")); + expect(joined.length).toBeLessThan(5); + expect(joined).toContain("tail"); +}); diff --git a/packages/compiler/src/native/text-file.ts b/packages/compiler/src/native/text-file.ts index d377c5273..8b0953120 100644 --- a/packages/compiler/src/native/text-file.ts +++ b/packages/compiler/src/native/text-file.ts @@ -42,6 +42,24 @@ export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator } } +/** `parts` with consecutive parts joined into strings of about + * TEXT_WRITE_LENGTH UTF-16 units (a longer part passes through whole), so + * writing or escaping many small parts takes few calls. */ +export function* joinedParts(parts: readonly string[]): Generator { + let batch: string[] = []; + let length = 0; + for (const part of parts) { + batch.push(part); + length += part.length; + if (length >= TEXT_WRITE_LENGTH) { + yield batch.length === 1 ? batch[0]! : batch.join(""); + batch = []; + length = 0; + } + } + if (batch.length > 0) yield batch.join(""); +} + /** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large * part is never encoded whole. `written` sees each slice after it is * written. */ @@ -55,8 +73,8 @@ function writeTextPart(fd: number, part: string, written?: (slice: string) => vo } } -/** Write text parts to `path` in order without joining them first; pass - * batched parts (`batchTextParts`) for many small ones. */ +/** Write text parts to `path` in order without joining them whole: small + * parts are written in batches (`joinedParts`). */ export function writeTextPartsSync( path: string, parts: readonly string[], @@ -64,7 +82,7 @@ export function writeTextPartsSync( ): void { const fd = openSync(path, "w"); try { - for (const part of parts) writeTextPart(fd, part, written); + for (const part of joinedParts(parts)) writeTextPart(fd, part, written); } finally { closeSync(fd); } From 7b79d73a550e530a4a20541ef5428e854a331fa5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:52:38 -0500 Subject: [PATCH 080/169] Store local number arrays of proven int32s in 4-byte slots A local number[] that only compiled element operations reach, and whose every stored element the int32 slot analysis proves an int32 (never -0), is now stored as SCR_ELEM_I32: 4-byte int32 slots, with state bytes once it holds a hole or undefined, as for every array. Element reads give consumers an exact int32, so indexing with an element skips the double round trip, and the slots take half the memory. The runtime's dense slot accessors convert between the int32 slot and the double, so its double-valued entry points serve int32 arrays unchanged; growth, copies, fill and length changes use the 4-byte width. ir/int32-arrays.ts proposes the candidates and inlines their element-reader helper calls; the int32 slot analysis keeps the proven ones, and selectInt32Arrays rewrites the module for exactly those, so a program without an int32 array emits the same LLVM IR as before. --- .../llvm/appended-array-literals.test.ts | 7 +- .../src/backend/llvm/dense-array-access.ts | 119 ++++- .../src/backend/llvm/emit-function.ts | 1 + .../src/backend/llvm/emit-statements.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 115 +++-- .../src/backend/llvm/expr-containers.ts | 31 +- .../src/backend/llvm/expr-primitives.ts | 52 ++- .../src/backend/llvm/local-array-reads.ts | 47 +- .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/int32-arrays.ts | 442 ++++++++++++++++++ packages/compiler/src/ir/int32-slots.ts | 55 ++- packages/compiler/src/ir/integer-ranges.ts | 146 +++++- packages/compiler/src/ir/ir.ts | 5 + .../test/int32-element-array-emission.test.ts | 130 ++++++ .../corpus/7326-int32-element-arrays.ts.json | 6 + packages/runtime/src/scr_array.c | 7 +- packages/runtime/src/scr_array.h | 48 +- packages/runtime/test/test_array.c | 59 +++ tests/corpus/7326-int32-element-arrays.ts | 165 +++++++ .../7326-int32-element-arrays.ts.json | 1 + 20 files changed, 1335 insertions(+), 106 deletions(-) create mode 100644 packages/compiler/src/ir/int32-arrays.ts create mode 100644 packages/compiler/test/int32-element-array-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json create mode 100644 tests/corpus/7326-int32-element-arrays.ts create mode 100644 tests/harness/corpus-costs/7326-int32-element-arrays.ts.json diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.test.ts b/packages/compiler/src/backend/llvm/appended-array-literals.test.ts index c483b7848..11a9704f6 100644 --- a/packages/compiler/src/backend/llvm/appended-array-literals.test.ts +++ b/packages/compiler/src/backend/llvm/appended-array-literals.test.ts @@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () = expect(found.has(idle)).toBe(false); expect(found.has(returned)).toBe(false); - // Only `fill` builds arrays. + // Only `fill` builds arrays. They may hold int32 slots (element kind 7), + // which does not change the choice of constructor. const llvm = emitLlvmModule(module(body, ["xs", "ys"])); - expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2); - expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2); + expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2); + expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2); }); test("a binding with the same id in another function does not count", () => { diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 9158d12cd..cc8d21ced 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -239,9 +239,10 @@ function statePointer( return statePtr; } -/** How an array's dense slots hold its elements: the access kind, or - * `cref` for references to compact class instances (SCR_ELEM_CREF: a 4-byte - * slot holding the address over 8; LlEmitter.compactArrayElem). */ +/** How an array's dense slots hold its elements: the access kind (`i32` + * for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact + * class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8; + * LlEmitter.compactArrayElem). */ type SlotKind = ArrayAccess | "cref"; function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind { @@ -249,14 +250,14 @@ function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): } /** The dense slot type: boolean arrays store one byte per element, compact - * references four, every other element kind an 8-byte slot. */ + * references and int32 arrays four, every other element kind an 8-byte slot. */ function slotType(kind: SlotKind): "i8" | "i32" | "i64" { - return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64"; + return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64"; } /** Bytes per dense slot (scr_arr_slot_width). */ -function slotWidth(kind: SlotKind): number { - return kind === "bool" ? 1 : kind === "cref" ? 4 : 8; +export function slotWidth(kind: SlotKind): number { + return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8; } /** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */ @@ -264,6 +265,17 @@ export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): numbe return slotWidth(slotKind(host, elemAccess(element), element)); } +/** The runtime accessor family of an access: int32 arrays answer through + * the double-valued entry points, which convert their slots. */ +export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" { + return acc === "i32" ? "f64" : acc; +} + +/** The LLVM type of an access's values at the emitter's interface. */ +function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" { + return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr"; +} + function valuePointer( host: LlvmEmitterContext, guard: Pick, @@ -285,7 +297,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string { return state; } -export type ArrayAccess = "f64" | "bool" | "ref"; +/** The ScrElemKind of int32 arrays (scr_array.h). */ +export const SCR_ELEM_I32 = 7; + +/** How a dense array's slots are read and written: doubles, booleans, + * references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts), + * whose values are doubles at the emitter's interface. */ +export type ArrayAccess = "f64" | "bool" | "ref" | "i32"; /** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference * element: when `index` lies in the hole-free prefix and the slot still @@ -341,15 +359,33 @@ export function emitDenseArrayGet( retain: boolean, proven = false, ): string { + return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven) + .value; +} + +/** emitDenseArrayGet, with the int32 of an int32 array's element: the + * slot itself on the dense path, the runtime getter's double converted + * (exactly: the element is an int32) on the other. */ +export function emitDenseArrayRead( + host: LlvmEmitterContext, + array: string, + index: LlValue, + indexExpr: IrExpr | undefined, + acc: ArrayAccess, + element: IrType, + getter: string, + retain: boolean, + proven = false, +): { value: string; int32: string | null } { const B = host.B; - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const ty = valueType(acc); const ret = acc === "bool" ? "zeroext i1" : ty; const kind = slotKind(host, acc, element); host.declare(`declare ${ret} @${getter}(ptr, double)`); if (acc === "ref" && retain && !isRefCounted(element)) { const t = B.tmp(); B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`); - return t; + return { value: t, int32: null }; } const slow = B.newLabel("arr.get.slow"), join = B.newLabel("arr.get.join"), @@ -380,10 +416,16 @@ export function emitDenseArrayGet( B.startBlock(valueLabel); const ptr = valuePointer(host, guard, kind); let fast: string; + let fastInt: string | null = null; let fastLabel = valueLabel; if (acc === "f64") { fast = B.tmp(); B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + } else if (acc === "i32") { + fastInt = B.tmp(); + fast = B.tmp(); + B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + B.line(`${fast} = sitofp i32 ${fastInt} to double`); } else if (acc === "bool") { const raw = B.tmp(); fast = B.tmp(); @@ -410,11 +452,21 @@ export function emitDenseArrayGet( const slowValue = B.tmp(); const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK; B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`); + let slowInt: string | null = null; + if (fastInt !== null) { + slowInt = B.tmp(); + B.line(`${slowInt} = fptosi double ${slowValue} to i32`); + } B.br(join); B.startBlock(join); const result = B.tmp(); B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`); - return result; + let int32: string | null = null; + if (fastInt !== null && slowInt !== null) { + int32 = B.tmp(); + B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`); + } + return { value: result, int32 }; } /** `scr_arr_get_number`: a numeric element, or NaN for every missing, @@ -425,13 +477,14 @@ export function emitDenseArrayNumber( index: LlValue, indexExpr: IrExpr | undefined, proven = false, + acc: "f64" | "i32" = "f64", ): string { return emitDenseArrayGet( host, array, index, indexExpr, - "f64", + acc, { kind: "f64" }, "scr_arr_get_number", false, @@ -498,9 +551,9 @@ export function emitDenseArraySet( proven = false, ): void { const B = host.B; - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const ty = valueType(acc); const kind = slotKind(host, acc, element); - const setter = `scr_arr_set_${acc}`; + const setter = `scr_arr_set_${runtimeAccess(acc)}`; host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`); const call = (attrs: string): void => B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`); @@ -508,6 +561,13 @@ export function emitDenseArraySet( call(""); return; } + // An int32 array's stores are proven int32s (ir/int32-arrays.ts); both + // dense paths below store the converted value. + let int32Value: string | null = null; + if (acc === "i32") { + int32Value = B.tmp(); + B.line(`${int32Value} = fptosi double ${value} to i32`); + } const slow = B.newLabel("arr.set.slow"), join = B.newLabel("arr.set.join"), filledLabel = B.newLabel("arr.set.filled"); @@ -526,6 +586,8 @@ export function emitDenseArraySet( const storeValue = (valuePtr: string): void => { if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (acc === "i32") { + B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); @@ -713,12 +775,21 @@ export function emitDenseArrayPop( B.line(`${wide} = zext i8 ${raw} to i64`); raw = wide; } else if (slotTy === "i32") { - // The 8-byte slot of a compact reference is its address. - const wide = B.tmp(), - address = B.tmp(); - B.line(`${wide} = zext i32 ${raw} to i64`); - B.line(`${address} = shl nuw nsw i64 ${wide}, 3`); - raw = address; + if (kind === "cref") { + // The 8-byte slot of a compact reference is its address. + const wide = B.tmp(), + address = B.tmp(); + B.line(`${wide} = zext i32 ${raw} to i64`); + B.line(`${address} = shl nuw nsw i64 ${wide}, 3`); + raw = address; + } else { + // The slot bits of an int32 element's double, as the f64 path returns them. + const number = B.tmp(), + bits = B.tmp(); + B.line(`${number} = sitofp i32 ${raw} to double`); + B.line(`${bits} = bitcast double ${number} to i64`); + raw = bits; + } } B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); // Only an array with states has a state byte to clear. @@ -755,8 +826,8 @@ export function emitDenseArrayPush( const B = host.B; const size = host.sizeType; const kind = element === null ? acc : slotKind(host, acc, element); - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; - const pusher = `scr_arr_push_${acc}`; + const ty = valueType(acc); + const pusher = `scr_arr_push_${runtimeAccess(acc)}`; host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`); const header = (field: number): string => { const ptr = B.tmp(); @@ -794,6 +865,10 @@ export function emitDenseArrayPush( const valuePtr = valuePointer(host, { offset: len, data }, kind); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (acc === "i32") { + const int32 = B.tmp(); + B.line(`${int32} = fptosi double ${value} to i32`); + B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 16e654393..2484bc38d 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -61,6 +61,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); emitter.B = B; emitter.currentFunctionName = fn.name; + emitter.findInt32Allocations(fn); emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control // returns to a synchronous frame (emitPendingCheck). diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index b530f794d..11d375a55 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -411,7 +411,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const v = emitter.emitExpr(s.value); if (s.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); - const acc = elemAccess(s.arr.type.elem); + const acc = emitter.arrayAccess(s.arr, s.arr.type.elem); emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); if (acc === "ref") emitter.toHeap(v); emitDenseArraySet( @@ -478,7 +478,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(dropsLast, pop, call); B.startBlock(pop); const elem = s.arr.type.elem; - const acc = elemAccess(elem); + const acc = emitter.arrayAccess(s.arr, elem); const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); if (acc === "ref" && isRefCounted(elem)) { const old = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..8c6c61953 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -79,6 +79,7 @@ import { } from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; +import { allocatedLiteral, selectInt32Arrays } from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; @@ -147,7 +148,7 @@ import { } from "./inline-unions.js"; import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js"; import { CheckedNarrows } from "./checked-narrows.js"; -import { emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js"; +import { emitDenseArraySet, emitDenseSlotUnlink, type ArrayAccess } from "./dense-array-access.js"; import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js"; import { type MapEntryFusion } from "./map-entry-fusion.js"; import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js"; @@ -483,6 +484,15 @@ function versionParameters(mod: IrModule): IrModule { return versionLoopBoundParameters(mod, mangleFunction); } +/** The int32 arrays the int32 slot analysis keeps in `mod`, under the + * inputs LlEmitter gives it (selectInt32Arrays). */ +function provenInt32Arrays(mod: IrModule): ReadonlySet { + return analyzeInt32Slots(mod, { + dynamicStores: dynamicFieldStores(mod), + initializerBindings: findInitializerBindings(mod), + }).int32Arrays; +} + export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { // Facts are keyed by node identity: emit the same module whether it was // lowered in memory or read back from its serialized form. @@ -490,28 +500,31 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) // Keep source storage intact for debugger inspection in dev builds. return new LlEmitter( options.debugSources === undefined - ? versionParameters( - addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + ? selectInt32Arrays( + versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + ), ), ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ), + provenInt32Arrays, ) : mod, options, @@ -528,28 +541,31 @@ export function emitLlvmModuleSource( const mod = unshareNodes(input); const parts = new LlEmitter( options.debugSources === undefined - ? versionParameters( - addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads( - freezeInitOnlyGlobals( - holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + ? selectInt32Arrays( + versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads( + freezeInitOnlyGlobals( + holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + ), ), ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ), + provenInt32Arrays, ) : mod, options, @@ -734,6 +750,53 @@ export class LlEmitter { return `, !tbaa !${tag}`; } + /** The current function's int32 array allocations: the array literals + * its int32 arrays are written with. They construct SCR_ELEM_I32 arrays. */ + int32Allocations = new Set(); + /** The fill temporaries (`new Array(n).fill(v)`) of those allocations: + * the same arrays, so int32 too. */ + int32ArrayTemps = new Set(); + + /** Collect the int32 allocations of `fn`, about to be emitted. */ + findInt32Allocations(fn: IrFunction): void { + this.int32Allocations.clear(); + this.int32ArrayTemps.clear(); + if (this.int32Slots.int32Arrays.size === 0) return; + everyStmtList(fn.body, { + stmt: (s) => { + if ( + (s.kind === "varDecl" || s.kind === "assign") && + this.int32Slots.int32Arrays.has(`${fn.name}\0${s.localId}`) + ) { + const value = s.kind === "varDecl" ? s.init : s.value; + const allocation = value && allocatedLiteral(value); + if (allocation) { + this.int32Allocations.add(allocation.literal); + if (allocation.temporary !== null) this.int32ArrayTemps.add(allocation.temporary); + } + } + return true; + }, + expr: () => true, + }); + } + + /** Whether `arr` is one of the current function's int32 arrays + * (ir/int32-arrays.ts, kept by the int32 slot analysis). */ + int32Array(arr: IrExpr): boolean { + if (arr.kind !== "varRef" || this.int32Slots.int32Arrays.size === 0) return false; + return ( + this.int32ArrayTemps.has(arr.localId) || + this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`) + ); + } + + /** The access an element operation on `arr` (of element type `elem`) + * takes: int32 arrays read and write their 4-byte slots. */ + arrayAccess(arr: IrExpr, elem: IrType): ArrayAccess { + return this.int32Array(arr) ? "i32" : elemAccess(elem); + } + /** Whether an element access at `index` of `arr` lies in the hole-free * prefix: the counter of a dense array loop's fast copy indexing one of * the arrays its entry test covered. */ @@ -6518,7 +6581,7 @@ export class LlEmitter { return tagInSet(this, uName, unionId, tags); } - arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string { + arrPush(arr: string, acc: ArrayAccess, value: string, element: IrType | null): string { return arrPush(this, arr, acc, value, element); } diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 2f4cd98e9..5ec780277 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -60,7 +60,10 @@ import { emitDenseArrayNumber, emitDenseArrayPop, emitDenseArrayPush, + runtimeAccess, + type ArrayAccess, } from "./dense-array-access.js"; +import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js"; import { emitUnionWiden } from "./expr-records.js"; import { registerReplay } from "./emit-journal.js"; @@ -115,7 +118,7 @@ export function tagInSet( export function arrPush( host: LlvmEmitterContext, arr: string, - acc: "f64" | "bool" | "ref", + acc: ArrayAccess, value: string, element: IrType | null, ): string { @@ -126,7 +129,7 @@ export function arrPush( export function emitArrayValues( host: LlvmEmitterContext, arr: string, - acc: "f64" | "bool" | "ref", + acc: ArrayAccess, element: IrType, values: LlValue[], prepend = false, @@ -142,12 +145,15 @@ export function emitArrayValues( } if (values.length === 1) { if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element); - const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const flat = runtimeAccess(acc); + const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr"; host.declare( - `declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`, + `declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`, ); const result = B.tmp(); - B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`); + B.line( + `${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`, + ); return result; } const capacity = Math.min(values.length, 64); @@ -165,8 +171,9 @@ export function emitArrayValues( const slot = B.tmp(), pointer = B.tmp(); const value = values[start + i]!.name; + // The runtime takes double bits for int32 arrays too, and converts. const pack = - acc === "f64" + acc === "f64" || acc === "i32" ? `bitcast double ${value} to i64` : acc === "bool" ? `zext i1 ${value} to i64` @@ -610,7 +617,12 @@ export function emitArrIntrinsic( if (e.receiver.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array"); const elem = e.receiver.type.elem; - const acc = elemAccess(elem); + // Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others + // take the runtime family's flat access. + const access = host.arrayAccess(e.receiver, elem); + if (access === "i32" && !INT32_ARRAY_METHODS.has(e.method)) + throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`); + const acc = runtimeAccess(access); const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; const accArg = acc === "bool" ? "i1 zeroext" : accTy; const method = e.method; @@ -639,6 +651,7 @@ export function emitArrIntrinsic( index, e.args[0], host.provenDenseIndex(e.receiver, e.args[0]!), + access === "i32" ? "i32" : "f64", ), type: e.type, }; @@ -675,7 +688,7 @@ export function emitArrIntrinsic( // no-op zero-argument call. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type }; + return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type }; } case "pushSpread": { // `a.push(...src)`: append src's elements in order (borrowed src, @@ -749,7 +762,7 @@ export function emitArrIntrinsic( let slowLabel = ""; if (method === "pop") { slowLabel = B.newLabel("remove.slow"); - denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel); + denseRaw = emitDenseArrayPop(host, r.name, access, elem, la, slowLabel); denseLabel = B.currentLabel(); B.br(lp); B.startBlock(slowLabel); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 72392956a..7d259f412 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -20,6 +20,7 @@ import { } from "./integer-values.js"; import { bitwiseIdentityOperand, + INT32_RANGE, integerArithmeticRange, isBitwiseOp, withinInt32, @@ -32,7 +33,15 @@ import { emitPowerOfTwoRemainder, emitSignedIntegerRemainder, } from "./integer-remainder.js"; -import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js"; +import { + denseSlotWidth, + emitDenseArrayRead, + emitDenseArrayState, + runtimeAccess, + SCR_ELEM_I32, + slotWidth, + type ArrayAccess, +} from "./dense-array-access.js"; import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; export function emitLiteralExpr( @@ -886,9 +895,23 @@ export function emitContainerExpr( // the plain elements up to the next one (scr_arr_reserve_spread). const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i)); const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread)); - B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`); + // An int32 array's allocation (ir/int32-arrays.ts) constructs + // SCR_ELEM_I32 storage; its elements are proven int32s. + const int32 = host.int32Allocations.has(e); + if (int32) { + host.declare( + small + ? `declare ptr @scr_arr_new_small(i32)` + : `declare ptr @scr_arr_new(i32, ${host.sizeType})`, + ); + B.line( + small + ? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})` + : `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`, + ); + } else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`); const out = host.own({ name: arr, type: e.type }); - const acc = elemAccess(elem); + const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem); let pending: LlValue[] = []; let buffer: string | undefined; const flush = (): void => { @@ -909,7 +932,8 @@ export function emitContainerExpr( B.line( `call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`, ); - host.emitArrayCopyLoop(arr, v.name, acc); + // A literal with spreads is never an int32 allocation. + host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc)); return; } pending.push(host.emitExpr(el)); @@ -954,19 +978,27 @@ export function emitContainerExpr( // Ref-element reads return +1 (the inline dense path retains like // the runtime getter); own registers the owned temp in the frame. const elem = e.arr.type.elem; - const acc = elemAccess(elem); - const t = emitDenseArrayGet( + const acc = host.arrayAccess(e.arr, elem); + const read = emitDenseArrayRead( host, arr.name, idx, e.index, acc, elem, - `scr_arr_get_${acc}`, + `scr_arr_get_${runtimeAccess(acc)}`, true, host.provenDenseIndex(e.arr, e.index), ); - return host.own({ name: t, type: e.type }); + if (read.int32 !== null) + // An int32 array's element: its i32 is exact for index consumers. + return { + name: read.value, + type: e.type, + uint32: read.int32, + integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE }, + }; + return host.own({ name: read.value, type: e.type }); } case "arrayHas": { const arr = host.emitStableReceiver(e.arr, [e.index]); @@ -979,7 +1011,7 @@ export function emitContainerExpr( idx, e.index, "has", - denseSlotWidth(host, e.arr.type.elem), + host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } @@ -994,7 +1026,7 @@ export function emitContainerExpr( idx, e.index, "state", - denseSlotWidth(host, e.arr.type.elem), + host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 8df187736..759d20927 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -476,7 +476,8 @@ function emitDenseReferenceArrayRead( no: string, slow: string, join: string, - loadType: "ptr" | "cref" | "i64" | "i8" = "ptr", + /** `i32`: an int32 array's slot, presented as the int32. */ + loadType: "ptr" | "cref" | "i64" | "i8" | "i32" = "ptr", ): void { const B = host.B; const header = (field: number): string => { @@ -567,7 +568,7 @@ function emitDenseReferenceArrayRead( statePtr, data, cap, - loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8, + loadType === "i8" ? 1 : loadType === "cref" || loadType === "i32" ? 4 : 8, offset, ); host.markMemoryPointer(statePtr, "array:present"); @@ -578,9 +579,10 @@ function emitDenseReferenceArrayRead( B.startBlock(valueLabel); const valuePtr = B.tmp(), raw = B.tmp(); - // Boolean arrays store one byte per element, compact references four, - // every other kind an 8-byte slot. - const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64"; + // Boolean arrays store one byte per element, compact references and int32 + // arrays four, every other kind an 8-byte slot. + const slotType = + loadType === "i8" ? "i8" : loadType === "cref" || loadType === "i32" ? "i32" : "i64"; B.line( `${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`, ); @@ -743,8 +745,15 @@ export function emitLocalArrayRead( const no = B.newLabel("local.array.missing"), join = B.newLabel("local.array.join"); const scalar = isScalarElement(read.element); + // An int32 array's arms join on the int32 element, converted to the + // payload's double once at the join, so a consumer converting the payload + // back to an integer folds to the element. + const int32 = scalar && read.element.kind === "f64" && host.int32Array(read.array); + const element = int32 ? B.slot() : null; + if (element) B.entryAllocas.push(`${element} = alloca i32`); const storeValue = (value: string): void => { - if (scalar) { + if (element) B.line(`store i32 ${value}, ptr ${element}`); + else if (scalar) { // Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool. let bits = value; if (read.element.kind === "bool") { @@ -772,7 +781,13 @@ export function emitLocalArrayRead( no, slow, join, - !scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64", + !scalar + ? referenceLoad(host, read.array.type) + : read.element.kind === "bool" + ? "i8" + : int32 + ? "i32" + : "i64", ); else B.br(slow); B.startBlock(slow); @@ -799,7 +814,8 @@ export function emitLocalArrayRead( B.line( `${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`, ); - B.line(`${bits} = bitcast double ${value} to i64`); + if (element) B.line(`${bits} = fptosi double ${value} to i32`); + else B.line(`${bits} = bitcast double ${value} to i64`); } else { host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`); B.line( @@ -807,7 +823,8 @@ export function emitLocalArrayRead( ); B.line(`${bits} = zext i1 ${value} to i64`); } - B.line(`store i64 ${bits}, ptr ${payload}`); + if (element) B.line(`store i32 ${bits}, ptr ${element}`); + else B.line(`store i64 ${bits}, ptr ${payload}`); B.line(`store i32 ${read.presentTag}, ptr ${tag}`); } else { const value = B.tmp(), @@ -821,10 +838,20 @@ export function emitLocalArrayRead( } B.br(join); B.startBlock(no); - B.line(`store i64 0, ptr ${payload}`); + if (element) B.line(`store i32 0, ptr ${element}`); + else B.line(`store i64 0, ptr ${payload}`); B.line(`store i32 ${read.missingTag}, ptr ${tag}`); B.br(join); B.startBlock(join); + if (element) { + const value = B.tmp(), + number = B.tmp(), + bits = B.tmp(); + B.line(`${value} = load i32, ptr ${element}`); + B.line(`${number} = sitofp i32 ${value} to double`); + B.line(`${bits} = bitcast double ${number} to i64`); + B.line(`store i64 ${bits}, ptr ${payload}`); + } B.line(`store ptr ${box}, ptr ${localSlot}`); return read.borrow || scalar ? null : { slot: payload, type: read.element }; } diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 7bef40aa6..45d046db9 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -208,6 +208,7 @@ const MODULE_FIELDS: Record>, Role> = { "contracts.int32Returns[]": "fact", "contracts.int32Locals[]": "other", "contracts.int32Elements[]": "other", + "int32ArrayCandidates[]": "other", "contracts.noThrow[]": "fact", "contracts.acyclic[]": "other", "contracts.immortalGlobals[]": "other", diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts new file mode 100644 index 000000000..82dcf4470 --- /dev/null +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -0,0 +1,442 @@ +import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { reparseExpr } from "./serialize.js"; +import { + everyExprChild, + everyStmtChild, + transformExpr, + transformStmt, + transformStmtList, + type IrTransform, +} from "./traverse.js"; + +/** Local number arrays the backend may store as int32 slots. + * + * A candidate is a local `number[]` of a synchronous function that no + * other binding can hold and only compiled element operations reach: + * every write of the local stores a fresh array (an array literal, or + * `new Array(n).fill(v)`), and every read of it is the receiver of an + * element read, write, state test, `length`, `push` or `pop` (a discarded + * `pop` included), or the array of an element-reader helper call. The + * array never reaches a runtime function that reads its slots, a library + * call, a closure, a return or untyped code. + * + * Candidates are only proposed here. The int32 slot analysis + * (int32-slots.ts) keeps the ones whose every stored element is a proven + * int32 (never -0), assuming the candidates' own elements are int32s, at + * a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. Their + * element-reader helper calls are inlined first (rewriteCandidates), so + * no shared helper ever receives an int32 array. */ + +/** Array methods a candidate may be the receiver of: the runtime paths + * behind them read and write int32 slots through the double-valued slot + * accessors. */ +export const INT32_ARRAY_METHODS: ReadonlySet = new Set([ + "length", + "push", + "pop", + "getNumber", +]); + +/** An element-reader helper (`%arr.idxOr.*`): `(a, i) => a's state at i is + * a value ? the element : undefined`, as one returned ternary. */ +export function isElementReader(fn: IrFunction): boolean { + if (fn.params.length !== 2 || fn.body.length !== 1) return false; + const [array, index] = fn.params; + const ret = fn.body[0]!; + if (array!.type.kind !== "array" || array!.type.elem.kind !== "f64") return false; + if (index!.type.kind !== "f64" || ret.kind !== "return" || ret.value?.kind !== "ternary") + return false; + const isParam = (e: IrExpr, id: string): boolean => e.kind === "varRef" && e.localId === id; + const { cond, then, else_ } = ret.value; + return ( + cond.kind === "bin" && + cond.op === "===" && + cond.left.kind === "arrayState" && + isParam(cond.left.arr, array!.localId) && + isParam(cond.left.index, index!.localId) && + cond.right.kind === "numLit" && + cond.right.value === 1 && + then.kind === "unionWrap" && + then.value.kind === "arrayGet" && + isParam(then.value.arr, array!.localId) && + isParam(then.value.index, index!.localId) && + else_.kind === "unionWrap" && + else_.value.kind === "unitLit" + ); +} + +/** `new Array(n).fill(v)` as lowering spells it: `fill` over a sequence + * that declares a temporary empty array, sets its length and yields it. */ +function fillAllocation(e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null { + if (e.kind !== "arrIntrinsic" || e.method !== "fill" || e.receiver.kind !== "seqExpr") + return null; + const seq = e.receiver; + const [decl, length] = seq.stmts; + if ( + seq.stmts.length !== 2 || + decl?.kind !== "varDecl" || + decl.init?.kind !== "arrayLit" || + decl.init.elems.length !== 0 || + length?.kind !== "arraySetLength" || + length.arr.kind !== "varRef" || + length.arr.localId !== decl.localId || + seq.result.kind !== "varRef" || + seq.result.localId !== decl.localId + ) + return null; + return e; +} + +/** A fresh number array: a literal without spreads, or the fill form. */ +export function int32ArrayAllocation(e: IrExpr): boolean { + return (e.kind === "arrayLit" && (e.spreads?.length ?? 0) === 0) || fillAllocation(e) !== null; +} + +/** The array literal a candidate's allocation builds (the literal itself, + * or the fill form's temporary's), and the fill form's temporary. */ +export function allocatedLiteral( + e: IrExpr, +): { literal: IrExpr & { kind: "arrayLit" }; temporary: string | null } | null { + if (e.kind === "arrayLit") return { literal: e, temporary: null }; + const fill = fillAllocation(e); + if (!fill || fill.receiver.kind !== "seqExpr") return null; + const decl = fill.receiver.stmts[0]!; + return decl.kind === "varDecl" && decl.init?.kind === "arrayLit" + ? { literal: decl.init, temporary: decl.localId } + : null; +} + +/** `xs.pop()` whose value is unused, as lowering spells it: a sequence + * that binds the array to a hidden local, then only reads that local's + * length and truncates it with pop stores. Returns the hidden local and the + * array read it binds. */ +function discardedPop(e: IrExpr): { local: string; array: IrExpr & { kind: "varRef" } } | null { + if (e.kind !== "seqExpr") return null; + const [bind, ...rest] = e.stmts; + if (bind?.kind !== "varDecl" || bind.init?.kind !== "varRef") return null; + const local = bind.localId; + const isLocal = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === local; + const expr = (x: IrExpr): boolean => { + if (x.kind === "arrIntrinsic" && x.method === "length" && isLocal(x.receiver)) return true; + return !isLocal(x) && everyExprChild(x, expr, stmt); + }; + const stmt = (x: IrStmt): boolean => { + if (x.kind === "arraySetLength" && x.pop === true && isLocal(x.arr)) return expr(x.length); + return ( + !((x.kind === "varDecl" || x.kind === "assign") && x.localId === local) && + everyStmtChild(x, expr, stmt) + ); + }; + return rest.every(stmt) && expr(e.result) ? { local, array: bind.init } : null; +} + +/** Whether evaluating `e` may assign the local `id`. */ +function writesLocal(e: IrExpr, id: string): boolean { + const expr = (x: IrExpr): boolean => + !(x.kind === "assignExpr" && x.localId === id) && everyExprChild(x, expr, stmt); + const stmt = (x: IrStmt): boolean => + !((x.kind === "varDecl" || x.kind === "assign") && x.localId === id) && + everyStmtChild(x, expr, stmt); + return !expr(e); +} + +/** The candidate locals of `fn` (see the module comment). */ +export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet): Set { + const result = new Set(); + if (fn.async || fn.generator) return result; + const captured = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + const params = new Set(fn.params.map((p) => p.localId)); + for (const local of fn.locals) + if ( + local.type.kind === "array" && + local.type.elem.kind === "f64" && + !local.boxed && + !local.tdz && + !captured.has(local.id) && + !params.has(local.id) + ) + result.add(local.id); + if (result.size === 0) return result; + const written = new Set(); + // A receiver position: the candidate is read there without escaping. + const receiver = (e: IrExpr): void => { + if (e.kind !== "varRef") expr(e); + }; + const expr = (e: IrExpr): boolean => { + switch (e.kind) { + case "varRef": + result.delete(e.localId); + return true; + case "assignExpr": + result.delete(e.localId); + break; + case "arrayGet": + case "arrayState": + case "arrayHas": + receiver(e.arr); + expr(e.index); + return true; + case "arrIntrinsic": + if (!INT32_ARRAY_METHODS.has(e.method)) break; + receiver(e.receiver); + e.args.forEach(expr); + return true; + case "seqExpr": { + // The pop's hidden local is the array itself; every use of it is a + // length read or a pop store. + const pop = discardedPop(e); + if (!pop) break; + e.stmts.slice(1).every(stmt); + expr(e.result); + return true; + } + case "call": { + if (!readers.has(e.callee) || e.args.length !== 2) break; + const [array, index] = e.args; + // Inlining evaluates the index before the array read. + if (array!.kind === "varRef" && writesLocal(index!, array!.localId)) + result.delete(array!.localId); + receiver(array!); + expr(index!); + return true; + } + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + switch (s.kind) { + case "varDecl": + case "assign": { + const value = s.kind === "varDecl" ? s.init : s.value; + if (value && int32ArrayAllocation(value)) written.add(s.localId); + else if (value) result.delete(s.localId); + if (value) expr(value); + return true; + } + case "arraySet": + receiver(s.arr); + expr(s.index); + expr(s.value); + return true; + case "arraySetUndefined": + // Stores a present undefined: a state, and a cleared (+0) slot. + receiver(s.arr); + expr(s.index); + return true; + case "forOf": + result.delete(s.localId); + break; + case "tryCatch": + if (s.catchLocalId) result.delete(s.catchLocalId); + break; + } + return everyStmtChild(s, expr, stmt); + }; + fn.body.every(stmt); + for (const id of result) if (!written.has(id)) result.delete(id); + return result; +} + +/** The element values stored into each candidate: element writes, pushed + * values, literal elements and fill values. */ +export function int32ArrayStores( + fn: IrFunction, + candidates: ReadonlySet, +): Map { + const stores = new Map(); + const add = (id: string, value: IrExpr): void => { + const list = stores.get(id); + if (list) list.push(value); + else stores.set(id, [value]); + }; + const target = (e: IrExpr): string | null => + e.kind === "varRef" && candidates.has(e.localId) ? e.localId : null; + const expr = (e: IrExpr): boolean => { + if (e.kind === "arrIntrinsic" && e.method === "push") { + const id = target(e.receiver); + if (id !== null) for (const arg of e.args) add(id, arg); + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if ((s.kind === "varDecl" || s.kind === "assign") && candidates.has(s.localId)) { + const value = s.kind === "varDecl" ? s.init : s.value; + if (value?.kind === "arrayLit") for (const el of value.elems) add(s.localId, el); + else if (value) { + const fill = fillAllocation(value); + if (fill) add(s.localId, fill.args[0]!); + } + } + if (s.kind === "arraySet") { + const id = target(s.arr); + if (id !== null) add(id, s.value); + } + return everyStmtChild(s, expr, stmt); + }; + fn.body.every(stmt); + return stores; +} + +/** Rewrite a function's candidates for the backend. A discarded pop uses + * the candidate in place of its hidden local. An element-reader helper call + * whose array is a candidate is inlined: the helper's returned ternary, with + * the candidate for its array and the index for its index, so the read means + * the same. An index that is not a plain read or literal is bound to a fresh + * local first: in its own declaration just before the statement when the + * call is the whole value of a declaration or assignment (the form lowering + * gives `a[i]!`, and the one the backend reads without a box), otherwise in a + * sequence around the ternary. Selection keeps out indexes that write the + * array, so evaluating the index before the array read is unobservable. */ +function rewriteCandidates( + fn: IrFunction, + candidates: ReadonlySet, + readers: ReadonlyMap, +): IrFunction { + const locals = [...fn.locals]; + let fresh = 0; + const bind = (index: IrExpr): { decl: IrStmt; ref: IrExpr } => { + const id = `%int32.read.${fresh++}`; + locals.push({ id, name: id, type: F64, mutable: false }); + return { + decl: { kind: "varDecl", localId: id, init: index, loc: index.loc }, + ref: { kind: "varRef", localId: id, type: F64, loc: index.loc }, + }; + }; + const readerCall = (e: IrExpr | null): (IrExpr & { kind: "call" }) | null => + e?.kind === "call" && + e.args.length === 2 && + readers.has(e.callee) && + e.args[0]!.kind === "varRef" && + candidates.has(e.args[0]!.localId) + ? e + : null; + const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit"; + // The statements of a list, with complex read indexes declared first. + const hoist = (list: IrStmt[]): IrStmt[] => + list.flatMap((s): IrStmt[] => { + if (s.kind !== "varDecl" && s.kind !== "assign") return [s]; + const value = s.kind === "varDecl" ? s.init : s.value; + const call = readerCall(value); + if (!call || simple(call.args[1]!)) return [s]; + const { decl, ref } = bind(call.args[1]!); + const read: IrExpr = { ...call, args: [call.args[0]!, ref] }; + return [decl, s.kind === "varDecl" ? { ...s, init: read } : { ...s, value: read }]; + }); + const lists = (s: IrStmt): IrStmt => { + switch (s.kind) { + case "if": + return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) }; + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + return { ...s, body: hoist(s.body) }; + case "switch": + return { ...s, cases: s.cases.map((c) => ({ ...c, body: hoist(c.body) })) }; + case "tryCatch": + return { + ...s, + tryBody: hoist(s.tryBody), + catchBody: s.catchBody === null ? null : hoist(s.catchBody), + finallyBody: s.finallyBody === null ? null : hoist(s.finallyBody), + }; + } + return s; + }; + // Hidden pop locals replaced by their candidate array. + const popLocals = new Set(); + const rewrite = (e: IrExpr): IrExpr => { + const pop = discardedPop(e); + if (e.kind === "seqExpr" && pop && candidates.has(pop.array.localId)) { + popLocals.add(pop.local); + const array = pop.array; + const substitute: IrTransform = { + expr: (x) => + x.kind === "varRef" && x.localId === pop.local ? { ...array, loc: x.loc } : x, + stmt: (x) => x, + }; + return { + ...e, + stmts: e.stmts.slice(1).map((x) => transformStmt(x, substitute)), + result: transformExpr(e.result, substitute), + }; + } + const call = readerCall(e); + if (!call) return e.kind === "seqExpr" ? { ...e, stmts: hoist(e.stmts) } : e; + const helper = readers.get(call.callee)!; + const array = call.args[0]!; + const [arrayParam, indexParam] = helper.params; + const ret = helper.body[0]!; + if (ret.kind !== "return" || !ret.value) return e; + let index = call.args[1]!; + const stmts: IrStmt[] = []; + if (!simple(index)) { + const { decl, ref } = bind(index); + stmts.push(decl); + index = ref; + } + const operand = index; + const body = transformExpr(reparseExpr(ret.value), { + expr: (x) => { + if (x.kind !== "varRef") return x; + if (x.localId === arrayParam!.localId) return reparseExpr(array); + if (x.localId === indexParam!.localId) return reparseExpr(operand); + return x; + }, + stmt: (x) => x, + }); + const typed: IrExpr = { ...body, loc: e.loc }; + return stmts.length === 0 + ? typed + : { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc }; + }; + const body = transformStmtList(hoist(fn.body), { expr: rewrite, stmt: lists }); + if (fresh === 0 && popLocals.size === 0) return { ...fn, body }; + return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) }; +} + +/** Propose the module's int32 array candidates (`function\0local` keys in + * `int32ArrayCandidates`), only those in `keep` when given, and rewrite + * their functions. */ +export function markInt32ArrayCandidates( + mod: IrModule, + keep: ReadonlySet | null = null, +): IrModule { + if (mod.lib !== undefined) return mod; + const readers = new Map(); + for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); + const names = new Set(readers.keys()); + const keys: string[] = []; + const functions = mod.functions.map((fn) => { + const candidates = int32ArrayCandidates(fn, names); + for (const id of candidates) + if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`); + else candidates.delete(id); + return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers); + }); + return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys }; +} + +/** The module the backend emits: rewritten for exactly the candidates the + * int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a + * module without int32 arrays is `mod` itself. A dropped candidate's + * rewrite is undone by marking again without it, until every candidate + * marked is kept. */ +export function selectInt32Arrays( + mod: IrModule, + prove: (marked: IrModule) => ReadonlySet, +): IrModule { + let keep: ReadonlySet | null = null; + for (let round = 0; round < 4; round++) { + const marked = markInt32ArrayCandidates(mod, keep); + if (marked === mod) return mod; + const kept = prove(marked); + const proposed = marked.int32ArrayCandidates ?? []; + if (proposed.every((key) => kept.has(key))) return marked; + keep = new Set(proposed.filter((key) => kept.has(key))); + } + return mod; +} diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index ac0747276..bb6a06867 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -10,6 +10,7 @@ import { } from "./integer-ranges.js"; import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js"; +import { int32ArrayStores } from "./int32-arrays.js"; import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an @@ -169,7 +170,10 @@ export class Int32Slots { this.int32Locals.has(`${fnName}\0${localId}`) || this.int32Locals.has(`\0${localId}`) ? INT32_RANGE : null; - if (this.int32Elements.size > 0) result.elements = (arr) => this.hasInt32Elements(fnName, arr); + if (this.int32Elements.size > 0) { + result.elements = (arr) => this.hasInt32Elements(fnName, arr); + if (this.narrowedReads) result.narrowedReads = true; + } const params = this.paramFacts.get(fnName); if (params) result.params = @@ -234,6 +238,19 @@ export class Int32Slots { addInt32Elements(key: string): void { this.int32Elements.add(key); } + dropInt32Elements(key: string): boolean { + return this.int32Elements.delete(key); + } + hasInt32ElementSlot(key: string): boolean { + return this.int32Elements.has(key); + } + /** Whether the module has int32 array candidates (the narrowedReads + * range fact). */ + narrowedReads = false; + /** Local number arrays (`function\0local`) stored as int32 slots: the + * module's int32 array candidates whose every stored element the + * fixpoint proved an int32. */ + readonly int32Arrays = new Set(); /** Whether every present element of the number array `arr` evaluates to * (read inside `fnName`) is an int32. */ hasInt32Elements(fnName: string, arr: IrExpr): boolean { @@ -752,6 +769,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key); for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key); + // Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32 + // elements until a store the fixpoint cannot prove drops one. + const arrayCandidates = new Map>(); + slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0; + for (const key of mod.int32ArrayCandidates ?? []) { + const cut = key.indexOf("\0"); + const fnName = key.slice(0, cut); + const set = arrayCandidates.get(fnName) ?? new Set(); + set.add(key.slice(cut + 1)); + arrayCandidates.set(fnName, set); + slots.addInt32Elements(`l:${key}`); + } /* ── candidate fields ── */ const children = new Map(); @@ -1031,6 +1060,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): return false; }; const hasCandidateWrite = (fn: IrFunction): boolean => { + if (arrayCandidates.has(fn.name)) return true; const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId)); const returns = slots.hasReturn(fn.name); return !everyStmtList(fn.body, { @@ -1076,6 +1106,25 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): continue; } const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e)); + // Every element stored into an int32 array candidate must be an int32 + // under the facts that assume the candidates' own elements are; a + // failing store drops its array, and the function is analyzed again + // without it. + const arrays = arrayCandidates.get(fn.name); + if (arrays) { + let dropped = false; + for (const [id, values] of int32ArrayStores(fn, arrays)) { + const key = `l:${fn.name}\0${id}`; + if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; + slots.dropInt32Elements(key); + reason(`array:${fn.name}\0${id}`, "an element store is not int32"); + dropped = true; + } + if (dropped && !queued.has(fn.name)) { + queued.add(fn.name); + queue.push(fn.name); + } + } // An int32 written into a slot still assumed non-negative: widen the // assumption when the value may be negative, and reanalyze its readers. const settle = (slot: string, e: IrExpr): void => { @@ -1144,6 +1193,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } for (const proven of provenByFunction.values()) for (const value of proven) slots.provenWrites.add(value); + for (const [fnName, ids] of arrayCandidates) + for (const id of ids) + if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`)) + slots.int32Arrays.add(`${fnName}\0${id}`); const ranged = rangedParameters(slots, functions, retry, numeric, (callee) => readers.get(`ret:${callee}`), ); diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 1d295ca24..0e5ca426d 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -220,6 +220,11 @@ export interface IntegerSlotFacts { /** Number arrays whose every present element is an int32 (Int32[] * contracts): an element read that found a value reads an int32. */ elements?: (arr: IrExpr) => boolean; + /** Element reads narrowed out of a union (`xs[i]!`, also when a union + * local holds the read) keep the element's range, and a ternary arm that + * always throws yields no value. Only modules with int32 array candidates + * (ir/int32-arrays.ts) set it, so no other program's ranges change. */ + narrowedReads?: boolean; } /** A local read, or a numeric local plus or minus an integer literal @@ -240,6 +245,50 @@ function offsetRef(e: IrExpr): { localId: string; offset: number } | null { return null; } +/** Library calls that always throw. */ +const THROWING_LIB = /^error\.\w*Throw$/; + +function throws(e: IrExpr): boolean { + return e.kind === "libCall" && THROWING_LIB.test(e.fn); +} + +/** The values written to each union-typed local of `fn` by declarations + * and assignments; null for a local that another write reaches or that a + * closure captures. */ +function unionLocalWrites( + fn: IrFunction, + captures: ReadonlySet, +): Map { + const unionLocals = new Set( + fn.locals + .filter((l) => l.type.kind === "union" && !l.boxed && !captures.has(l.id)) + .map((l) => l.id), + ); + const result = new Map(); + const add = (id: string, value: IrExpr | null): void => { + if (!unionLocals.has(id)) return; + const list = result.get(id); + if (list === null) return; + if (value === null) result.set(id, null); + else if (list) list.push(value); + else result.set(id, [value]); + }; + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind === "varDecl" && s.init) add(s.localId, s.init); + else if (s.kind === "assign") add(s.localId, s.value); + else if (s.kind === "forOf") add(s.localId, null); + else if (s.kind === "tryCatch" && s.catchLocalId) add(s.catchLocalId, null); + return true; + }, + expr: (e) => { + if (e.kind === "assignExpr") add(e.localId, null); + return true; + }, + }); + return result; +} + /** Structured local dataflow. Calls cannot write uncaptured local slots, * but argument expressions can; unknown evaluation forms invalidate all * syntactically written slots before their children are inspected. Loops @@ -272,6 +321,10 @@ export function analyzeIntegerRangeFacts( .map((l) => l.id), ); const loopLocals = new Map(fn.locals.filter((l) => !captures.has(l.id)).map((l) => [l.id, l])); + // With narrowed reads, the values written to union locals (an element + // read held before it is narrowed), or null for a local some other write + // (assignExpr, for-of, catch, capture) reaches. + const unionWrites = slots.narrowedReads ? unionLocalWrites(fn, captures) : null; // A captured binding has no local dataflow: only a proof that covers // every write of the shared box anywhere applies to its reads. const boxedRange = (localId: string): IntegerRange | null => { @@ -490,6 +543,45 @@ export function analyzeIntegerRangeFacts( return range; } + /** narrowedRange through a union local: every value written to it. */ + function narrowedValue( + value: IrExpr, + unionId: string, + tag: number, + ): IntegerRange | null | "never" { + if (value.kind !== "varRef") return narrowedRange(value, unionId, tag); + const written = unionWrites?.get(value.localId); + if (!written || written.length === 0) return null; + let result: IntegerRange | null | "never" = "never"; + for (const w of written) { + const arm = narrowedRange(w, unionId, tag); + if (arm === "never") continue; + if (arm === null) return null; + result = result === "never" ? arm : union(result, arm); + } + return result; + } + /** The range of the values of union tag `tag` that `value` (already + * analyzed) can hold: "never" when no arm holds that tag. */ + function narrowedRange( + value: IrExpr, + unionId: string, + tag: number, + ): IntegerRange | null | "never" { + if (value.kind === "seqExpr") return narrowedRange(value.result, unionId, tag); + if (value.kind === "ternary") { + const yes = narrowedRange(value.then, unionId, tag), + no = narrowedRange(value.else_, unionId, tag); + if (yes === "never") return no; + if (no === "never") return yes; + return yes && no ? union(yes, no) : null; + } + if (value.kind === "unionWrap" && value.unionId === unionId) { + if (value.tag !== tag) return "never"; + return value.value.kind === "arrayGet" ? (ranges.get(value.value) ?? null) : null; + } + return null; + } function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null { // Shared IR objects have to satisfy the proof at every occurrence. const previous = ranges.get(e); @@ -683,6 +775,10 @@ export function analyzeIntegerRangeFacts( right = expr(e.else_, no); merge(facts, yes, no); if (left && right) range = union(left, right); + // With narrowed reads: an arm that always throws yields no value, + // so the other arm's range is the result's. + else if (slots.narrowedReads && throws(e.then)) range = right; + else if (slots.narrowedReads && throws(e.else_)) range = left; break; } case "logical": { @@ -777,27 +873,45 @@ export function analyzeIntegerRangeFacts( expr(e.obj, facts); range = slotRange(e); break; + case "unionNarrow": + // Only modules with narrowed reads take this rule: the narrow + // evaluates its operand first, and throws for every arm of another + // tag, so an element read `xs[i]!` (a ternary of the wrapped + // element and a wrapped undefined) has the element's range. + if (!slots.narrowedReads || e.type.kind !== "f64") { + range = conservative(e, facts); + break; + } + expr(e.value, facts); + { + const arm = narrowedValue(e.value, e.unionId, e.tag); + range = arm === "never" ? null : arm; + } + break; default: - // Do not assume the traversal order is an evaluation order for an - // unknown node. Every child gets the same conservative entry state. - havoc(facts, [e], facts); - everyExprChild( - e, - (child) => { - expr(child, new Map(facts)); - return true; - }, - (child) => { - body([child], new Map(facts)); - return true; - }, - ); - // A direct call's result carries its whole-program return proof. - range = slotRange(e); + range = conservative(e, facts); break; } return remember(e, range); } + /** An unknown node: do not assume the traversal order is an evaluation + * order. Every child gets the same conservative entry state. */ + function conservative(e: IrExpr, facts: Facts): IntegerRange | null { + havoc(facts, [e], facts); + everyExprChild( + e, + (child) => { + expr(child, new Map(facts)); + return true; + }, + (child) => { + body([child], new Map(facts)); + return true; + }, + ); + // A direct call's result carries its whole-program return proof. + return slotRange(e); + } function induction( s: IrStmt & { kind: "for" }, facts: Facts, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 12400fee9..0fa873a16 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1073,6 +1073,11 @@ export interface IrModule { * write in the program (ir/contract-check.ts). Backends may rely on them * unconditionally. Absent when the program writes no contract. */ contracts?: IrContracts; + /** Local number arrays (`function\0local`) the backend may store as + * int32 slots, proposed by ir/int32-arrays.ts after their element-reader + * calls were inlined; the int32 slot analysis keeps the ones whose every + * stored element it proves an int32. Absent when there is none. */ + int32ArrayCandidates?: string[]; /** The WebAssembly modules translated into this program (wasm/ * translate.ts), present exactly when the program uses the WebAssembly * API (possibly empty). The backend appends each module's translation diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts new file mode 100644 index 000000000..8e89c8562 --- /dev/null +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -0,0 +1,130 @@ +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 type { IrModule } from "../src/ir/ir.js"; +import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js"; + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +async function lowerIr(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const source = ` +function compose(n: number, rounds: number): number { + const p: number[] = []; + const q: number[] = []; + for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1)); + for (let i = 0; i < n; i++) q.push(i); + let check = 0; + for (let r = 0; r < rounds; r++) { + for (let i = 0; i < n; i++) q[i] = p[q[i]!]!; + check = (check + q[r % n]!) | 0; + } + return check; +} +function counts(n: number): number { + const c = new Array(n).fill(0) as number[]; + for (let i = 0; i < n; i++) c[(i * 5) % n] = i; + while (c.length > 1) c.pop(); + return c[0]!; +} +function fraction(n: number): number { + const a = [1, 2, 3]; + a[1] = n / 3; + return a[1]!; +} +function negativeZero(): number { + const a = [1, -0]; + return 1 / a[1]!; +} +function counter(n: number): number { + const a = [0]; + for (let i = 0; i < n; i++) a[0] = a[0]! + 1; + return a[0]!; +} +function joined(n: number): string { + const a = [n | 0, 2]; + return a.join(","); +} +const { n } = JSON.parse('{"n": 8}') as { n: number }; +console.log(compose(n | 0, 3), counts(n | 0), fraction(n), negativeZero(), counter(n), joined(n)); +`; + +/** Every function emitted for the source function \`name\` (its int32 ABI + * body and its wrapper included). */ +function functions(llvm: string, name: string): string { + const pattern = new RegExp(`^define [^@]*@sc_b?f_${name}[.\\w]*\\([^]*?^}`, "gm"); + return [...llvm.matchAll(pattern)].map((m) => m[0]).join("\n"); +} + +test("arrays storing only int32 elements get 4-byte slots", async () => { + const llvm = await emitLlvm(source); + const compose = functions(llvm, "compose"); + expect(compose).not.toBe(""); + // Both arrays are allocated as SCR_ELEM_I32 (7). + expect(compose.match(/@scr_arr_new_small\(i32 7\)/g)?.length).toBe(2); + // Reads load an i32 slot; writes store one. + expect(compose).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/); + expect(compose).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/); + // The element read through the helper's ternary stays in a stack box. + expect(compose).not.toContain("@scr_union_new_f64"); + expect(compose).not.toMatch(/call [^\n]*@sc_b?f__x25_arr_idxOr/); + // The fill form and a discarded pop. + const counts = functions(llvm, "counts"); + expect(counts).toMatch(/@scr_arr_new\(i32 7, i64 0\)/); + expect(counts).not.toContain("popArray"); +}); + +test("arrays with a store that may not be an int32, or that escape, keep doubles", async () => { + const llvm = await emitLlvm(source); + for (const name of ["fraction", "negativeZero", "counter", "joined"]) { + const fn = functions(llvm, name); + expect(fn, name).not.toBe(""); + expect(fn, name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/); + } +}); + +test("only the candidates the proof keeps are rewritten", async () => { + const mod = await lowerIr(source); + const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? []; + expect(proposed).toEqual( + expect.arrayContaining(["compose\0p.0", "compose\0q.0", "counts\0c.0", "counter\0a.0"]), + ); + // An array that reaches `join` is never proposed. + expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false); + // Without a kept candidate, the module is the input itself. + expect(selectInt32Arrays(mod, () => new Set())).toBe(mod); + const kept = new Set(["compose\0p.0", "compose\0q.0"]); + const selected = selectInt32Arrays( + mod, + (marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))), + ); + expect(selected.int32ArrayCandidates).toEqual([...kept]); + for (const fn of selected.functions) + if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name)); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json new file mode 100644 index 000000000..f00823a6e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7326-int32-element-arrays.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index f3e216a36..bbe66e26e 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -277,6 +277,7 @@ typedef struct { static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) { if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index]; if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]); + if (s->elem == SCR_ELEM_I32) return scr_arr_f64_slot((double)((const int32_t *)s->data)[index]); return s->data[index]; } @@ -621,7 +622,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode); * paths pick the same capacity. */ static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) { if (need <= a->cap) return; - if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || + if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || a->elem == SCR_ELEM_I32 || (a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) { scr_arr_grow_dense_slow(a, need, mode); return; @@ -1904,6 +1905,10 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, if (state != SCR_ARR_VALUE) scr_arr_cut(a, from); if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from); else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); + else if (a->elem == SCR_ELEM_I32) { + int32_t v = scr_arr_i32_of(slot); + for (size_t i = from; i < until; i++) ((int32_t *)a->data)[i] = v; + } else for (size_t i = from; i < until; i++) a->data[i] = slot; if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from); } else { diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index e13acb233..6034462e9 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -48,6 +48,12 @@ typedef enum { * address over 8, 0 for NULL. Compact instances are never counted or * traced, so these arrays retain, release and trace nothing. */ SCR_ELEM_CREF, + /* Numbers that the compiler proves are always int32 (never -0), in + * 4-byte slots: arrays that only compiled element accesses ever reach + * (backend/llvm int32 arrays). Every runtime slot is the value's double + * bits, as for SCR_ELEM_F64: scr_arr_dense_get and scr_arr_dense_put + * convert, so the paths compiled code calls need no case of their own. */ + SCR_ELEM_I32, } ScrElemKind; typedef enum { @@ -134,11 +140,12 @@ typedef struct ScrArr { * 0 is always valid. */ uint32_t filled; /* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or - * `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in - * the same allocation, when the array has states (SCR_ARR_STATES), by one - * state byte per slot in reverse slot order (scr_arr_states_end). Sparse - * entries and properties keep 8-byte slots for every kind; a bool slot is - * 0 or 1 wherever it lives, and a CREF slot's 8-byte form is the pointer + * `cap` 4-byte slots for SCR_ELEM_CREF and SCR_ELEM_I32 + * (scr_arr_slot_width), followed in the same allocation, when the array + * has states (SCR_ARR_STATES), by one state byte per slot in reverse slot + * order (scr_arr_states_end). Sparse entries and properties keep 8-byte + * slots for every kind; a bool slot is 0 or 1 wherever it lives, a CREF + * slot's 8-byte form is the pointer and an I32 slot's the double's bits * (scr_arr_dense_get). Rarely * used storage (sparse indices, noncanonical numeric properties, * match-result metadata) lives out of line, and an array that has it @@ -260,7 +267,7 @@ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) { /* Bytes per dense value slot of element kind `elem`. */ static inline size_t scr_elem_slot_width(uint8_t elem) { if (elem == SCR_ELEM_BOOL) return 1; - if (__builtin_expect(elem == SCR_ELEM_CREF, 0)) return sizeof(uint32_t); + if (__builtin_expect(elem == SCR_ELEM_CREF || elem == SCR_ELEM_I32, 0)) return sizeof(uint32_t); return sizeof(uint64_t); } @@ -325,11 +332,38 @@ static inline void scr_arr_need_states(ScrArr *a) { static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; } static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); } +/* The bits of `d` as a slot, and back. */ +static inline uint64_t scr_arr_f64_slot(double d) { + uint64_t slot; + memcpy(&slot, &d, sizeof slot); + return slot; +} + +static inline double scr_arr_slot_f64(uint64_t slot) { + double d; + memcpy(&d, &slot, sizeof d); + return d; +} + +/* An int32 array's slot for a double: its stores are proven int32s that + * are not -0 (and a cleared slot is +0), so anything else is a compiler + * bug. */ +static inline int32_t scr_arr_i32_of(uint64_t slot) { + double d = scr_arr_slot_f64(slot); + if (__builtin_expect(!(d >= -2147483648.0 && d <= 2147483647.0 && d == (double)(int32_t)d) || + slot == UINT64_C(0x8000000000000000), + 0)) + __builtin_trap(); + return (int32_t)d; +} + /* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) return scr_cref_decode(((const uint32_t *)a->data)[index]); + if (__builtin_expect(a->elem == SCR_ELEM_I32, 0)) + return scr_arr_f64_slot((double)((const int32_t *)a->data)[index]); return a->data[index]; } @@ -337,6 +371,8 @@ static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) ((uint32_t *)a->data)[index] = scr_cref_encode(slot); + else if (__builtin_expect(a->elem == SCR_ELEM_I32, 0)) + ((int32_t *)a->data)[index] = scr_arr_i32_of(slot); else a->data[index] = slot; } diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 769b8ef3a..8cbac7f57 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -1614,6 +1614,64 @@ static bool inline_slots(const ScrArr *a) { return a->data == (const uint64_t *)((const char *)a + ((sizeof(ScrArr) + 7) & ~(size_t)7)); } +/* Int32 slots: 4-byte values, then the state bytes when the array has them. */ +static bool i32_layout_ok(const ScrArr *a) { + return a->elem == SCR_ELEM_I32 && scr_arr_slot_width(a) == sizeof(int32_t); +} + +/* SCR_ELEM_I32: 4-byte int32 slots behind the double-valued entry points. */ +static void test_i32_slots(void) { + ScrArr *a = scr_arr_new_small(SCR_ELEM_I32); + for (int i = 0; i < 1000; i++) scr_arr_push_f64(a, (double)(i * 7 - 3000)); + check(i32_layout_ok(a) && !scr_arr_has_states(a) && a->filled == 1000, + "int32 pushes grow four-byte storage without states"); + bool values_ok = true; + for (int i = 0; i < 1000; i++) { + values_ok &= ((const int32_t *)a->data)[i] == i * 7 - 3000; + values_ok &= scr_arr_get_f64(a, i) == (double)(i * 7 - 3000); + values_ok &= scr_arr_get_number(a, i) == (double)(i * 7 - 3000); + } + check(values_ok, "int32 values are stored as 4-byte integers and read as doubles"); + scr_arr_set_f64(a, 10, 2147483647); + scr_arr_set_f64(a, 11, -2147483648.0); + check(scr_arr_get_f64(a, 10) == 2147483647 && scr_arr_get_f64(a, 11) == -2147483648.0, + "int32 extremes round-trip"); + check(scr_arr_pop_f64(a) == 999 * 7 - 3000 && scr_arr_len(a) == 999, "pop reads the last int32"); + scr_arr_set_len(a, 5); + scr_arr_set_len(a, 8); + check(i32_layout_ok(a) && scr_arr_has_states(a) && scr_arr_state(a, 6) == SCR_ARR_HOLE && + isnan(scr_arr_get_number(a, 6)) && scr_arr_get_f64(a, 4) == 4 * 7 - 3000, + "int32 truncation clears slots and extension adds holes"); + scr_arr_set_f64(a, 20, 5); + check(scr_arr_len(a) == 21 && scr_arr_state(a, 15) == SCR_ARR_HOLE && scr_arr_get_f64(a, 20) == 5, + "an int32 store past the length leaves holes"); + scr_arr_release(scr_arr_fill_f64(a, -9, 0, 21)); + bool filled_ok = a->filled == 21; + for (int i = 0; i < 21; i++) filled_ok &= scr_arr_get_f64(a, i) == -9; + check(filled_ok, "int32 fill writes every slot"); + scr_arr_release(a); + + ScrArr *s = scr_arr_new(SCR_ELEM_I32, 0); + scr_arr_set_f64(s, 0, 1); + scr_arr_set_f64(s, 3000000, 3); + scr_arr_set_f64(s, 2000000, -2); + check(arr_sparse_len(s) == 2 && scr_arr_get_f64(s, 2000000) == -2 && scr_arr_get_f64(s, 3000000) == 3, + "int32 sparse entries keep double slots"); + scr_arr_set_len(s, 2000001); + scr_arr_release(scr_arr_fill_f64(s, 4, 1, 2000000)); + check(arr_sparse_len(s) == 0 && i32_layout_ok(s) && scr_arr_get_f64(s, 2000000) == -2 && + scr_arr_get_f64(s, 1999999) == 4 && scr_arr_get_f64(s, 0) == 1, + "densifying absorbs sparse entries into int32 slots"); + scr_arr_release(s); + + ScrArr *k = scr_arr_new(SCR_ELEM_I32, 6); + for (int i = 0; i < 6; i++) scr_arr_push_f64(k, i); + scr_arr_push_f64(k, 6); + check(i32_layout_ok(k) && scr_arr_get_f64(k, 6) == 6 && scr_arr_get_f64(k, 0) == 0, + "a known-count int32 array grows out of its block"); + scr_arr_release(k); +} + static void test_inline_slots(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); @@ -2472,6 +2530,7 @@ int main(int argc, char **argv) { test_splice_drop(); test_filled_prefix(); test_bool_bytes(); + test_i32_slots(); test_inline_slots(); test_head_room(); test_states_on_demand(); diff --git a/tests/corpus/7326-int32-element-arrays.ts b/tests/corpus/7326-int32-element-arrays.ts new file mode 100644 index 000000000..f5dc0d787 --- /dev/null +++ b/tests/corpus/7326-int32-element-arrays.ts @@ -0,0 +1,165 @@ +// Local number arrays whose every stored element is an int32: reads, writes, +// holes, present undefined, out-of-range and noncanonical indexes, growth, +// pop, the fill form, reassignment, and arrays that do not qualify. + +function compose(n: number, rounds: number): number { + const p: number[] = []; + const q: number[] = []; + for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1)); + for (let i = 0; i < n; i++) q.push(i); + let check = 0; + for (let r = 0; r < rounds; r++) { + for (let i = 0; i < n; i++) q[i] = p[q[i]!]!; + check = (check + q[r % n]!) | 0; + } + return check; +} + +function insertionSort(n: number): string { + const a: number[] = []; + let seed = 12345; + for (let i = 0; i < n; i++) { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + a.push((seed % 2001) - 1000); + } + for (let i = 1; i < n; i++) { + for (let j = i; j > 0 && a[j - 1]! > a[j]!; j--) { + const t = a[j]!; + a[j] = a[j - 1]!; + a[j - 1] = t; + } + } + let sorted = true; + for (let i = 1; i < n; i++) if (a[i - 1]! > a[i]!) sorted = false; + return `${sorted} ${a[0]} ${a[n >> 1]} ${a[n - 1]} ${a.length}`; +} + +function reads(): string[] { + const a = [7, -3, 2147483647, -2147483648, 0]; + const out: string[] = []; + out.push(`${a[0]} ${a[1]} ${a[2]} ${a[3]} ${a[4]} ${a[5]}`); + out.push(`${a[2]! + 1} ${a[3]! - 1} ${a[1]! / 2} ${a[2]! * 2}`); + const idx = [0, 4, 5, -1, 1.5, NaN, -0, 2 ** 32, Infinity]; + for (const i of idx) out.push(`${i}: ${a[i]} ${a[i] === undefined} ${i in a}`); + out.push(`len ${a.length}`); + return out; +} + +function holes(): string[] { + const a = [1, 2, 3]; + const out: string[] = []; + a[6] = 9; + out.push(`len ${a.length} ${a[3]} ${a[6]} ${4 in a} ${6 in a}`); + a[4] = -5; + let sum = 0, + present = 0; + for (let i = 0; i < a.length; i++) { + if (a[i] !== undefined) { + sum += a[i]!; + present++; + } + } + out.push(`sum ${sum} present ${present}`); + // A hole read through a non-null assertion stores a present undefined. + const q = [10, 20, 30]; + q[1] = a[3]!; + out.push(`q ${q[0]} ${q[1]} ${q[2]} ${1 in q} ${q.length}`); + q[1] = 21; + out.push(`q ${q[1]} ${(q[0]! + q[1]!) | 0}`); + return out; +} + +function sparse(): string[] { + const a = [1, 2]; + const out: string[] = []; + a[100000] = 42; + a[3] = 4; + out.push(`len ${a.length} ${a[100000]} ${a[3]} ${a[2]} ${a[99999]}`); + a[100000] = -42; + out.push(`${a[100000]} ${a[100001]}`); + return out; +} + +function growth(n: number): string[] { + const a: number[] = []; + const out: string[] = []; + for (let i = 0; i < n; i++) a.push((i * i) | 0); + out.push(`len ${a.length} ${a[0]} ${a[n - 1]} ${a[n]}`); + let sum = 0; + while (a.length > n / 2) sum = (sum + a.pop()!) | 0; + out.push(`popped ${sum} len ${a.length} last ${a[a.length - 1]}`); + for (let i = 0; i < 3; i++) a.push(i - 3); + out.push(`len ${a.length} ${a[a.length - 3]} ${a[a.length - 1]}`); + while (a.length > 0) a.pop(); + out.push(`empty ${a.length} ${a.pop()} ${a[0]}`); + a.push(5); + out.push(`again ${a.length} ${a[0]}`); + return out; +} + +function filled(n: number): string[] { + const counts = new Array(n).fill(0) as number[]; + const marks: number[] = new Array(n).fill(-1); + for (let i = 0; i < 3 * n; i++) { + const k = ((i * 37) % n) | 0; + counts[k] = (i * 3 - 7) | 0; + marks[(k + 1) % n] = k; + } + let a = 0, + b = 0; + for (let i = 0; i < n; i++) { + a = (a + counts[i]!) | 0; + b = (b + marks[i]!) | 0; + } + return [`fill ${counts.length} ${a} ${b} ${counts[n]} ${marks[0]}`]; +} + +function reassigned(rounds: number): string { + let a = [1, 2, 3]; + let total = 0; + for (let r = 0; r < rounds; r++) { + if (r % 3 === 0) a = [r, r + 1]; + else a.push(r); + total = (total + a[a.length - 1]! + a.length) | 0; + } + return `reassigned ${total} ${a.length} ${a[0]}`; +} + +// These arrays do not qualify; they print the same values as before. +function disqualified(): string[] { + const out: string[] = []; + const negZero = [0, -0]; + out.push(`${Object.is(negZero[1], -0)} ${1 / negZero[1]!}`); + const fraction = [1, 2]; + fraction[1] = 2.5; + out.push(`${fraction[1]}`); + const big = [1]; + big[0] = big[0]! + 2147483647; + out.push(`${big[0]}`); + const joined = [3, 1, 2]; + joined.sort((x, y) => x - y); + out.push(joined.join(",")); + const escaped = [4, 5]; + const alias = escaped; + alias[0] = 6; + out.push(`${escaped[0]} ${alias[1]}`); + const counter = [0]; + for (let i = 0; i < 10; i++) counter[0] = counter[0]! + 1; + out.push(`${counter[0]}`); + return out; +} + +const config = JSON.parse('{"n": 256, "rounds": 50, "sort": 300}') as { + n: number; + rounds: number; + sort: number; +}; +console.log(compose(config.n | 0, config.rounds | 0)); +console.log(insertionSort(config.sort | 0)); +for (const line of reads()) console.log(line); +for (const line of holes()) console.log(line); +for (const line of sparse()) console.log(line); +for (const line of growth(config.n | 0)) console.log(line); +for (const line of filled(17)) console.log(line); +console.log(reassigned(config.rounds | 0)); +for (const line of disqualified()) console.log(line); diff --git a/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json b/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json new file mode 100644 index 000000000..168ae57fb --- /dev/null +++ b/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 4100 } From b025254f7995563e58daaf89f19b334d84d0ebb7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:53:28 -0500 Subject: [PATCH 081/169] Keep the charAt unit reads' origins on the emitter The charCodeAt reads that stand for rewritten charAt reads (one-unit tests and switches) recorded the charAt each stands for in a module-level WeakMap. Compiler sources keep such caches on their owners: the map now belongs to the emitter (charAtUnitReads), and charAtUnitRead takes it. --- .../compiler/src/backend/llvm/emit-statements.ts | 9 ++++++--- packages/compiler/src/backend/llvm/emitter.ts | 4 ++++ .../compiler/src/backend/llvm/expr-containers.ts | 13 +++++-------- .../compiler/src/backend/llvm/expr-primitives.ts | 4 ++-- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index b530f794d..333715b97 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -1826,7 +1826,7 @@ export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" * case, refcounted/boxed case-body locals are NULL-reset up front and * the scope-exit releases rely on NULL tolerance. */ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void { - const unitSwitch = charAtUnitSwitch(s); + const unitSwitch = charAtUnitSwitch(emitter, s); if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch); const B = emitter.B; const discKind = s.disc.type.kind; @@ -2518,7 +2518,10 @@ function nullableStringSwitch( * one-unit string literals, as a switch on `s.charCodeAt(i)` with the units * as cases: no character string is built. An index out of range answers * NaN, which matches no case, as "" matches no one-unit literal. */ -function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null { +function charAtUnitSwitch( + emitter: LlEmitter, + s: IrStmt & { kind: "switch" }, +): (IrStmt & { kind: "switch" }) | null { const disc = s.disc; if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1) return null; @@ -2531,7 +2534,7 @@ function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "sw return null; return { ...s, - disc: charAtUnitRead(disc), + disc: charAtUnitRead(emitter, disc), cases: s.cases.map((c) => c.test?.kind === "strLit" ? { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..5eecda3ec 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1201,6 +1201,10 @@ export class LlEmitter { * (findIntegerOrNaNLocals in wrapping-sums.ts). */ integerOrNaNLocals: ReadonlyMap = new Map(); bytesBounds: ReadonlySet = new Set(); + /** The charCodeAt reads standing for charAt reads the emitter rewrote + * (one-unit tests and switches, charAtUnitRead), with the charAt each + * stands for: a loop version's proof for the charAt covers its read. */ + readonly charAtUnitReads = new Map(); /** Worker programs: typed-array accesses whose views an enclosing version * guard proved thread-owned. */ bytesOwned: ReadonlySet = new Set(); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 2f4cd98e9..b84e3fda5 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -286,14 +286,11 @@ export function emitArrayCopyLoop( host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`); } -/** The `charCodeAt` reads that stand for `charAt` reads the emitter - * rewrote, such as one-unit tests and switches: a loop version's proof - * for the charAt covers its read. */ -const unitReadOrigins = new WeakMap(); - /** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a - * code unit. */ + * code unit. The emitter records the charAt it stands for + * (charAtUnitReads), so a loop version's proof for the charAt covers it. */ export function charAtUnitRead( + host: LlvmEmitterContext, charAt: IrExpr & { kind: "strIntrinsic" }, ): IrExpr & { kind: "strIntrinsic" } { const read: IrExpr & { kind: "strIntrinsic" } = { @@ -304,7 +301,7 @@ export function charAtUnitRead( type: F64, loc: charAt.loc, }; - unitReadOrigins.set(read, charAt); + host.charAtUnitReads.set(read, charAt); return read; } @@ -312,7 +309,7 @@ export function charAtUnitRead( * below the length (bytes-loop-versions.ts). */ function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean { if (host.bytesBounds.has(e)) return true; - const origin = unitReadOrigins.get(e); + const origin = host.charAtUnitReads.get(e); return origin !== undefined && host.bytesBounds.has(origin); } diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 72392956a..0d57d3f69 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -648,7 +648,7 @@ export function emitStringExpr( // equals no unit, as "" equals no one-unit literal. const unit = charAtUnitTest(e); if (unit !== null) { - const code = host.emitExpr(charAtUnitRead(unit.charAt)); + const code = host.emitExpr(charAtUnitRead(host, unit.charAt)); const t = B.tmp(); B.line( `${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`, @@ -682,7 +682,7 @@ export function emitStringExpr( // the code unit, as the equality above does. const unit = charAtUnitTest(e); if (unit !== null) { - const code = host.emitExpr(charAtUnitRead(unit.charAt)); + const code = host.emitExpr(charAtUnitRead(host, unit.charAt)); const t = B.tmp(); B.line( `${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`, From 17bef8c03b505a5bbb934f06cd6f2a35b190eede Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:54:55 -0700 Subject: [PATCH 082/169] Type user code from npm package declarations at a checked, identity-preserving boundary Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/dyn.ts | 32 +- packages/compiler/src/backend/llvm/emitter.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../compiler/src/frontend/decl-projection.ts | 735 ++++ .../frontend/lowering/coercions/structural.ts | 17 + .../lowering/expressions/narrowing.ts | 74 + .../frontend/lowering/key-order-witness.ts | 9 +- .../compiler/src/frontend/lowering/lowerer.ts | 19 +- packages/compiler/src/frontend/program.ts | 148 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/ir.ts | 13 + packages/compiler/src/ir/validate.ts | 2 + packages/runtime/src/scr_dyn.h | 5 + packages/runtime/src/scr_json.c | 207 + .../fixtures/npm/cases/zod3-boundary/main.ts | 83 + .../zod3-boundary/node_modules/zod/LICENSE | 21 + .../node_modules/zod/dist/esm/index.js | 3 + .../node_modules/zod/dist/esm/package.json | 3 + .../node_modules/zod/dist/esm/v3/ZodError.js | 132 + .../node_modules/zod/dist/esm/v3/errors.js | 9 + .../node_modules/zod/dist/esm/v3/external.js | 6 + .../zod/dist/esm/v3/helpers/enumUtil.js | 1 + .../zod/dist/esm/v3/helpers/errorUtil.js | 6 + .../zod/dist/esm/v3/helpers/parseUtil.js | 109 + .../zod/dist/esm/v3/helpers/partialUtil.js | 1 + .../zod/dist/esm/v3/helpers/typeAliases.js | 1 + .../zod/dist/esm/v3/helpers/util.js | 133 + .../node_modules/zod/dist/esm/v3/index.js | 4 + .../zod/dist/esm/v3/locales/en.js | 107 + .../zod/dist/esm/v3/standard-schema.js | 1 + .../node_modules/zod/dist/esm/v3/types.js | 3691 +++++++++++++++++ .../node_modules/zod/dist/types/index.d.ts | 3 + .../zod/dist/types/v3/ZodError.d.ts | 164 + .../zod/dist/types/v3/errors.d.ts | 5 + .../zod/dist/types/v3/external.d.ts | 6 + .../zod/dist/types/v3/helpers/enumUtil.d.ts | 8 + .../zod/dist/types/v3/helpers/errorUtil.d.ts | 9 + .../zod/dist/types/v3/helpers/parseUtil.d.ts | 78 + .../dist/types/v3/helpers/partialUtil.d.ts | 8 + .../dist/types/v3/helpers/typeAliases.d.ts | 2 + .../zod/dist/types/v3/helpers/util.d.ts | 85 + .../node_modules/zod/dist/types/v3/index.d.ts | 4 + .../zod/dist/types/v3/locales/en.d.ts | 3 + .../zod/dist/types/v3/standard-schema.d.ts | 102 + .../node_modules/zod/dist/types/v3/types.d.ts | 1031 +++++ .../node_modules/zod/package.json | 120 + .../npm/cases/zod3-boundary/package.json | 1 + 48 files changed, 7196 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/src/frontend/decl-projection.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/main.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/cases/zod3-boundary/package.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..72e6c6c9e 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -153,6 +153,8 @@ export interface DynHost extends WalkerHost { /** See moduleUsesDynBuiltinStream. */ usesDynBuiltinStream?(): boolean; liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string; + /** The module has declaration-typed boundary casts (IrModule.declBoundary). */ + declBoundary?(): boolean; dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; classSubtypes(className: string): readonly string[]; @@ -4526,6 +4528,31 @@ export class LlDyn { /** The dyn argument spelling of one static value: dyn passes through * (+1), funcs box anonymously, everything else rides toDyn. `expr` is * BORROWED in every arm. Emits into the given builder. */ + /** A live capsule of a record or array value (+1; the value is + * borrowed), as `dynFrom` with `liveRef` builds it; null for other + * types. Closures boxed in a declaration-typed boundary program return + * their records and arrays this way. */ + private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null { + const host = this.host; + if (t.kind === "union") { + const arms = host.unionsById.get(t.unionId)?.arms ?? []; + if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null; + const boxed = B.tmp(); + B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`); + return boxed; + } + if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null; + const key = typeKey(t); + const adapter = host.liveDynRefAdapter(t); + const rc = vAdapters(host, t); + const boxed = B.tmp(); + B.line( + `${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`, + ); + for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line); + return boxed; + } + private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string { if (t.kind === "dyn") return this.retainDyn(B, expr); if (t.kind === "func") { @@ -4689,7 +4716,10 @@ export class LlDyn { const r = B.tmp(); B.line(`${r} = call ${retTy} ${fn}(${callArgs})`); this.pendingBail(B, "dfkr", () => {}, "ptr null"); - const out = this.toDynExpr(B, t.ret, r); + const out = + this.host.declBoundary?.() === true + ? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r)) + : this.toDynExpr(B, t.ret, r); if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`); B.terminate(`ret ptr ${out}`); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..8e14541af 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1597,6 +1597,7 @@ export class LlEmitter { liveDynRefAdapter: (type) => this.liveDynRefAdapter(type), usesDynBuiltinStream: () => this.usesDynBuiltinStream, liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type), + declBoundary: () => this.mod.declBoundary === true, dynPromiseAdapter: (type) => this.dynPromiseAdapter(type), isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error", classRootOf: (name) => this.classMeta.get(name)?.root.def.name, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..ecd3d3b5b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1570,6 +1570,7 @@ export const LIB_FN_SYMS: Record = { "dyn.objValueList": "scr_dyn_obj_value_list", "dyn.objEntries": "scr_dyn_obj_entries", "dyn.structuredClone": "scr_structured_clone", + "dyn.declAdopt": "scr_dyn_decl_adopt", "dyn.cloneMissing": "scr_structured_clone_missing", "dyn.cloneTransferFail": "scr_structured_clone_transfer_fail", "dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with", diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts new file mode 100644 index 000000000..923bfb428 --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -0,0 +1,735 @@ +/* The declaration-typed boundary's projection pass. + * + * An opted-in npm package compiles from its JavaScript, and the program the + * lowering compiles types it from that JavaScript: where the package's + * published declarations are richer (zod's `z.infer`), user code sees `any`. + * This pass asks the DECLARATION world (the same entry checked against the + * packages' declarations) for the types of those user-code positions. Where + * the declared type is pure data, it is printed as structural TypeScript into + * a generated global declaration file, and the user file is rewritten in the + * lowering world: + * + * - type positions that name package types (`z.infer`) name the + * projected alias instead; + * - package-produced expressions (`S.parse(x)`, `r.data`) are wrapped in + * `( as __scriptc_decl_N)`, the boundary cast; + * - untyped parameters of user callbacks the package calls gain the alias. + * + * The lowering world is then rebuilt from the rewritten text, so the + * checker's own inference carries the projected types through the rest of + * the user code, and the lowering sees an ordinary checked cast at each + * crossing (lowered with `adopt`, see narrowing.ts). No edit adds or removes + * a line break, so positions keep their lines. IR records are structural, + * so a printed type interns to the same shape as any equal type. + * + * Only programs whose user code has such positions are rewritten; every + * other program is untouched (the zero-cost rule). See + * scriptc-perf/results/design-decl-boundary/DESIGN.md. */ + +import * as ts from "./ts7/adapter.js"; + +/** Alias names of projected types start with this prefix; the lowering + * recognizes boundary casts and parameters by it. */ +export const DECL_BOUNDARY_PREFIX = "__scriptc_decl_"; + +/** The generated declaration file's base name. */ +export const DECL_BOUNDARY_FILE = "__scriptc_decl_boundary__.d.ts"; + +export function isDeclBoundaryFile(fileName: string): boolean { + return fileName.split("\\").join("/").endsWith(`/${DECL_BOUNDARY_FILE}`); +} + +export function isDeclBoundaryTypeName(name: string): boolean { + return name.startsWith(DECL_BOUNDARY_PREFIX); +} + +/** True for `T` in `x as T` / `(p: T) =>` when T names a projected alias. */ +export function isDeclBoundaryTypeNode(node: ts.TypeNode | undefined): boolean { + return ( + node !== undefined && + ts.isTypeReferenceNode(node) && + ts.isIdentifier(node.typeName) && + isDeclBoundaryTypeName(node.typeName.text) && + node.typeArguments === undefined + ); +} + +interface Edit { + start: number; + end: number; + text: string; + /** Tie-break among edits at one position (lower first). */ + rank?: number; +} + +export interface DeclProjection { + /** Rewritten text per user file. */ + files: Map; + /** The generated declaration file (path and text). */ + declFile: string; + declText: string; + /** Human-readable plan (SCRIPTC_DEBUG_DECL_BOUNDARY). */ + plan: string[]; +} + +export interface DeclProjectionInput { + jsProgram: ts.Program; + declProgram: ts.Program; + /** User source files to consider (TypeScript, outside node_modules). */ + userFiles: readonly string[]; + /** Directory of the generated declaration file. */ + outDir: string; +} + +function isPackageDeclarationFile(sf: ts.SourceFile | undefined): boolean { + if (sf === undefined || !sf.isDeclarationFile) return false; + const f = sf.fileName.split("\\").join("/"); + // The TypeScript standard library ships inside node_modules too. + if (/(^|\/)lib(\.[\w.-]+)?\.d\.ts$/.test(f) || f.startsWith("bundled:")) return false; + return ( + f.includes("/node_modules/") && + !f.includes("/node_modules/@types/node/") && + !f.includes("/node_modules/typescript/") && + !f.includes("/node_modules/@typescript/") + ); +} + +function declaredInPackage(symbol: ts.Symbol | undefined): boolean { + if (symbol === undefined) return false; + return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d))); +} + +function declaredInUserFile(symbol: ts.Symbol | undefined): boolean { + if (symbol === undefined) return false; + const decls = symbol.declarations; + return ( + decls.length > 0 && + decls.every((d) => { + const sf = ts.getSourceFileOfNode(d); + return !sf.isDeclarationFile && !sf.fileName.split("\\").join("/").includes("/node_modules/"); + }) + ); +} + +const ANYISH = ts.TypeFlags.Any | ts.TypeFlags.Unknown; +const READONLY_CHECK_FLAG = 8; + +/** Prints declaration-world types as structural TypeScript, or answers null + * when a type is not pure data (DESIGN.md §4.3). */ +class TypePrinter { + readonly aliases = new Map(); + readonly declarations: string[] = []; + private readonly inProgress = new Map(); + + constructor(private readonly checker: ts.TypeChecker) {} + + /** The alias naming a projectable type, or null. Top-level any/unknown + * never projects. */ + project(type: ts.Type): string | null { + if (type.flags & ANYISH) return null; + const text = this.print(type, 0); + if (text === null || text === "unknown") return null; + let name = this.aliases.get(text); + if (name === undefined) { + name = `${DECL_BOUNDARY_PREFIX}${this.aliases.size}`; + this.aliases.set(text, name); + this.declarations.push(`type ${name} = ${text};`); + } + return name; + } + + private print(type: ts.Type, depth: number): string | null { + if (depth > 24) return null; + const f = type.flags; + if (f & ANYISH) return "unknown"; + if (f & ts.TypeFlags.String) return "string"; + if (f & ts.TypeFlags.Number) return "number"; + if (f & ts.TypeFlags.Boolean) return "boolean"; + 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"; + if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value)); + if (f & ts.TypeFlags.NumberLiteral) { + const v = Number(type.value); + return Number.isFinite(v) ? String(v) : null; + } + if (type.isUnionType()) { + const parts: string[] = []; + for (const arm of type.getTypes()) { + const p = this.print(arm, depth + 1); + if (p === null || p === "unknown") return null; + if (!parts.includes(p)) parts.push(p); + } + // `true | false` reads better as boolean, and maps the same. + const t = parts.indexOf("true"); + const fl = parts.indexOf("false"); + if (t >= 0 && fl >= 0) { + parts.splice(Math.max(t, fl), 1); + parts.splice(Math.min(t, fl), 1, "boolean"); + } + return parts.map((p) => (/=>/.test(p) ? `(${p})` : p)).join(" | "); + } + if (type.isIntersectionType()) { + // Brands: drop members that only carry unique-symbol keys. + const kept = type.getTypes().filter((m) => !this.isBrandOnly(m)); + if (kept.length === 1) return this.print(kept[0]!, depth + 1); + if (kept.length === 0) return null; + if (!kept.every((m) => (m.flags & ts.TypeFlags.Object) !== 0)) return null; + return this.printObject(type, depth); + } + if (f & ts.TypeFlags.Object) return this.printObject(type, depth); + return null; + } + + private isBrandOnly(type: ts.Type): boolean { + if (!(type.flags & ts.TypeFlags.Object) || type.isIntersectionType()) return false; + const props = this.checker.getPropertiesOfType(type); + return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@")); + } + + private printObject(type: ts.Type, depth: number): string | null { + const checker = this.checker; + if (checker.isArrayType(type)) { + const [elem] = checker.getTypeArguments(type); + if (elem === undefined) return null; + const e = this.print(elem, depth + 1); + if (e === null) return null; + const ro = type.getSymbol()?.escapedName === "ReadonlyArray" ? "readonly " : ""; + return `${ro}(${e})[]`; + } + if (checker.isTupleType(type)) { + const target = type.getTarget() ?? type; + const flags = target.elementFlags ?? []; + if (flags.some((fl) => fl !== ts.ElementFlags.Required)) return null; + const elems = checker.getTypeArguments(type).slice(0, flags.length); + const out: string[] = []; + for (const e of elems) { + const p = this.print(e, depth + 1); + if (p === null) return null; + out.push(p); + } + return `${target.readonly === true ? "readonly " : ""}[${out.join(", ")}]`; + } + const symbol = type.getSymbol(); + if (symbol !== undefined) { + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method)) + return null; + // Interfaces: only user-declared ones describe plain data reliably + // (package and lib interfaces often describe class instances). + if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null; + } + if ( + checker.getCallSignatures(type).length > 0 || + checker.getConstructSignatures(type).length > 0 + ) + return null; + const memo = this.inProgress.get(type.id); + if (memo !== undefined) { + memo.name ??= `${DECL_BOUNDARY_PREFIX}r${type.id}`; + return memo.name; + } + const frame: { name: string | null } = { name: null }; + this.inProgress.set(type.id, frame); + try { + const members: string[] = []; + let readonlyCount = 0; + const props = checker.getPropertiesOfType(type); + for (const prop of props) { + const key = String(prop.escapedName); + if (key.startsWith("__@")) continue; // symbol-keyed (brands) + if ( + prop.flags & + (ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor) + ) + return null; + const propType = checker.getTypeOfSymbol(prop); + const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0; + let p = this.print(propType, depth + 1); + if (p === null) return null; + const readonly = (prop.checkFlags & READONLY_CHECK_FLAG) !== 0 || hasReadonlyModifier(prop); + if (readonly) readonlyCount++; + const name = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key); + if (optional && !/\bundefined\b/.test(p)) p = `${p} | undefined`; + members.push(`${readonly ? "readonly " : ""}${name}${optional ? "?" : ""}: ${p}`); + } + for (const info of checker.getIndexInfosOfType(type)) { + if (!(info.keyType.flags & ts.TypeFlags.String)) return null; + const v = this.print(info.type, depth + 1); + if (v === null) return null; + members.push(`[key: string]: ${v}`); + } + if (members.length === 0) return null; // `{}` admits any non-nullish value + // All-readonly object types may describe frozen values (.readonly()). + if (props.length > 0 && readonlyCount === props.length) return null; + const text = `{ ${members.join("; ")} }`; + if (frame.name !== null) { + this.declarations.push(`type ${frame.name} = ${text};`); + return frame.name; + } + return text; + } finally { + this.inProgress.delete(type.id); + } + } +} + +function hasReadonlyModifier(prop: ts.Symbol): boolean { + return prop.declarations.some( + (d) => + (ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) && + (d.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword), + ); +} + +/** Children of a node in source order (forEachChild order). */ +function childrenOf(node: ts.Node): ts.Node[] { + const out: ts.Node[] = []; + ts.forEachChild( + node, + (c) => { + out.push(c); + return undefined; + }, + (list) => { + for (const c of list.nodes) out.push(c); + return undefined; + }, + ); + return out; +} + +function isWriteTarget(node: ts.Node): boolean { + let child = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isBinaryExpression(parent) && parent.left === child) { + const k = parent.operatorToken.kind; + return k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment; + } + if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) { + const op = parent.operator; + return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken; + } + if (ts.isDeleteExpression(parent)) return true; + if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) return parent.initializer === child; + if (ts.isShorthandPropertyAssignment(parent)) return true; + if ( + ts.isArrayLiteralExpression(parent) || + ts.isPropertyAssignment(parent) || + ts.isSpreadElement(parent) + ) { + // Destructuring assignment targets: conservative, skip. + let up: ts.Node | undefined = parent; + while (up !== undefined && (ts.isArrayLiteralExpression(up) || ts.isObjectLiteralExpression(up) || ts.isPropertyAssignment(up) || ts.isSpreadElement(up))) + up = up.parent; + if (up !== undefined && ts.isBinaryExpression(up) && up.operatorToken.kind === ts.SyntaxKind.EqualsToken) + return true; + } + return false; +} + +function inOptionalChain(node: ts.Node): boolean { + const parent = node.parent; + if (parent === undefined) return false; + if ( + (ts.isPropertyAccessExpression(parent) || + ts.isElementAccessExpression(parent) || + ts.isCallExpression(parent)) && + parent.expression === node && + (parent.questionDotToken !== undefined || (parent.flags & ts.NodeFlags.OptionalChain) !== 0) + ) + return true; + return ( + (ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + ts.isCallExpression(node)) && + (node.flags & ts.NodeFlags.OptionalChain) !== 0 + ); +} + +function isCallee(node: ts.Node): boolean { + const p = node.parent; + return ( + p !== undefined && + (ts.isCallExpression(p) || ts.isNewExpression(p) || ts.isTaggedTemplateExpression(p)) && + (ts.isTaggedTemplateExpression(p) ? p.tag === node : p.expression === node) + ); +} + +function inTypeContext(node: ts.Node): boolean { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isTypeNode(p) || ts.isTypeQueryNode(p)) return true; + if (ts.isExpressionWithTypeArguments(p) && p.parent && ts.isHeritageClause(p.parent)) + return true; + if (ts.isStatement(p)) return false; + } + return false; +} + +function unwrapTransparent(node: ts.Expression): ts.Expression { + let e = node; + while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e)) + e = e.expression; + return e; +} + +/** Plans the projection for one program pair. Answers null when nothing + * in the user files projects (the program stays as it is). */ +export function planDeclProjection(input: DeclProjectionInput): DeclProjection | null { + const jsChecker = input.jsProgram.getTypeChecker(); + const declChecker = input.declProgram.getTypeChecker(); + const printer = new TypePrinter(declChecker); + const plan: string[] = []; + const edits = new Map(); + + const typeAt = (checker: ts.TypeChecker, node: ts.Node): ts.Type | null => { + try { + return checker.getTypeAtLocation(node); + } catch { + return null; + } + }; + const isAnyish = (t: ts.Type | null): boolean => t !== null && (t.flags & ANYISH) !== 0; + const resolveAlias = (s: ts.Symbol | undefined): ts.Symbol | undefined => { + if (s === undefined || !(s.flags & ts.SymbolFlags.Alias)) return s; + try { + return declChecker.getAliasedSymbol(s); + } catch { + return s; + } + }; + const declSymbol = (n: ts.Node): ts.Symbol | undefined => { + try { + return resolveAlias(declChecker.getSymbolAtLocation(n)); + } catch { + return undefined; + } + }; + const namesPackageSymbol = (node: ts.Node): boolean => { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + if (ts.isIdentifier(n) && declaredInPackage(declSymbol(n))) { + found = true; + return; + } + for (const c of childrenOf(n)) visit(c); + }; + visit(node); + return found; + }; + + for (const file of input.userFiles) { + const jsSf = input.jsProgram.getSourceFile(file); + 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(); + const exprOrder: ts.Node[] = []; + + const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => { + if ( + !ts.isTypeReferenceNode(jsNode) && + !ts.isIndexedAccessTypeNode(jsNode) && + !ts.isExpressionWithTypeArguments(jsNode) + ) + return false; + if (ts.isExpressionWithTypeArguments(jsNode)) { + if (!jsNode.parent || !ts.isHeritageClause(jsNode.parent)) return false; + if (!jsNode.parent.parent || !ts.isInterfaceDeclaration(jsNode.parent.parent)) return false; + } + let jsType: ts.Type | null; + let declType: ts.Type | null; + try { + jsType = ts.isExpressionWithTypeArguments(jsNode) + ? jsChecker.getTypeAtLocation(jsNode) + : jsChecker.getTypeFromTypeNode(jsNode as ts.TypeNode); + declType = ts.isExpressionWithTypeArguments(declNode) + ? declChecker.getTypeAtLocation(declNode) + : declChecker.getTypeFromTypeNode(declNode as ts.TypeNode); + } catch { + return false; + } + if (!isAnyish(jsType) || declType === null) return false; + if (!namesPackageSymbol(declNode)) return false; + const alias = printer.project(declType); + if (alias === null) return false; + const start = ts.tokenStartOfNode(jsNode); + fileEdits.push({ start, end: jsNode.end, text: alias }); + plan.push(`${file}:${start}: type ${jsSf.text.slice(start, jsNode.end)} -> ${alias}`); + return true; + }; + + const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => { + if (!ts.isExpression(jsNode)) return; + const isCall = + ts.isCallExpression(jsNode) || ts.isNewExpression(jsNode) || ts.isTaggedTemplateExpression(jsNode); + const isAccess = ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); + const isAwait = ts.isAwaitExpression(jsNode); + if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return; + if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return; + if (inOptionalChain(jsNode)) return; + const parent = jsNode.parent; + if (parent !== undefined && ts.isExpressionStatement(parent)) return; + if (parent !== undefined && (ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent))) + return; + if (ts.isIdentifier(jsNode)) { + if ( + parent !== undefined && + (ts.isPropertyAccessExpression(parent) && parent.name === jsNode) + ) + return; + } + const jsType = typeAt(jsChecker, jsNode); + if (!isAnyish(jsType)) return; + // Package-produced? + const d = declNode as ts.Expression; + let produced = false; + if (ts.isCallExpression(d) || ts.isNewExpression(d)) { + const callee = d.expression; + const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee; + produced = declaredInPackage(declSymbol(target)); + if (!produced) { + try { + const sig = declChecker.getResolvedSignature(d); + const decl = sig?.declaration; + produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)); + } catch { + produced = false; + } + } + } else if (ts.isTaggedTemplateExpression(d)) { + produced = declaredInPackage(declSymbol(d.tag)); + } 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; + produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs)); + } else if (ts.isAwaitExpression(d)) { + const inner = unwrapTransparent(d.expression); + produced = + (ts.isCallExpression(inner) || ts.isPropertyAccessExpression(inner)) && + (declaredInPackage( + declSymbol( + ts.isCallExpression(inner) + ? ts.isPropertyAccessExpression(inner.expression) + ? inner.expression.name + : inner.expression + : inner.name, + ), + ) || + isAnyish(typeAt(jsChecker, inner))); + } else if (ts.isIdentifier(d)) { + const raw = (() => { + try { + return declChecker.getSymbolAtLocation(d); + } catch { + return undefined; + } + })(); + produced = + raw !== undefined && + (raw.flags & ts.SymbolFlags.Alias) !== 0 && + declaredInPackage(resolveAlias(raw)); + } + if (!produced) return; + // A consumer that takes anything needs no conversion. + try { + const ctx = declChecker.getContextualType(d); + if (ctx !== undefined && ctx.flags & ANYISH) return; + } catch { + // 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); + exprOrder.push(jsNode); + }; + + const annotated = new Set(); + const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => { + if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) return; + const fn = jsNode.parent; + if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return; + if (jsNode.dotDotDotToken !== undefined) return; + const jsType = typeAt(jsChecker, jsNode.name); + if (!isAnyish(jsType)) return; + const declType = typeAt(declChecker, (declNode as ts.ParameterDeclaration).name); + if (declType === null) return; + // Only parameters the package's declarations type (a callback the + // package calls), not ones the user typed elsewhere. + try { + const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression); + if (ctxFn === undefined) return; + const sigs = declChecker.getCallSignatures(ctxFn); + if (!sigs.some((s) => s.declaration !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)))) + return; + } catch { + return; + } + const alias = printer.project(declType); + if (alias === null) return; + annotated.add(jsNode); + const name = jsNode.name; + const nameStart = ts.tokenStartOfNode(name); + const text = jsSf.text; + const bareArrow = + ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && text[ts.tokenStartOfNode(fn)] !== "(" && text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === "" ; + const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; + if (bareArrow) { + fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); + fileEdits.push({ start: afterName, end: afterName, text: `: ${alias})` }); + } else { + fileEdits.push({ start: afterName, end: afterName, text: `: ${alias}` }); + } + plan.push(`${file}:${nameStart}: parameter ${name.text}: ${alias}`); + }; + + const walk = (jsNode: ts.Node, declNode: ts.Node): void => { + if (jsNode.kind !== declNode.kind) return; + if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) { + if (considerType(jsNode, declNode)) return; + } else { + considerParam(jsNode, declNode); + considerExpr(jsNode, declNode); + } + const a = childrenOf(jsNode); + const b = childrenOf(declNode); + if (a.length !== b.length) return; + for (let i = 0; i < a.length; i++) walk(a[i]!, b[i]!); + }; + walk(jsSf, declSf); + + // Suppress candidates whose receiver chain another candidate types. + const symbolTyped = new Map(); + const willBeTyped = (e: ts.Node, depth = 0): boolean => { + if (depth > 32) return false; + if (exprCandidates.has(e)) return true; + if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e)) + return willBeTyped(e.expression, depth + 1); + if (ts.isAsExpression(e)) return false; + if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) + return willBeTyped(e.expression, depth + 1); + if (ts.isIdentifier(e)) { + let sym: ts.Symbol | undefined; + try { + sym = jsChecker.getSymbolAtLocation(e); + } catch { + return false; + } + if (sym === undefined) return false; + const known = symbolTyped.get(sym); + if (known !== undefined) return known; + symbolTyped.set(sym, false); + let result = false; + const decls = sym.declarations; + if (decls.length === 1) { + const decl = decls[0]!; + if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined && decl.type === undefined) + result = willBeTyped(decl.initializer, depth + 1); + else if (ts.isParameter(decl) && annotated.has(decl)) result = true; + else if (ts.isBindingElement(decl)) result = false; + } + symbolTyped.set(sym, result); + return result; + } + return false; + }; + 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[] = []; + for (const e of exprOrder) { + const recv = receiverOf(e); + if (recv !== null && willBeTyped(recv)) continue; + // An identifier candidate that is a user binding never gets here + // (only package imports are candidates). + kept.push(e); + } + // Nested candidates: keep only the innermost (an outer one is typed by + // the inner wrap). + const keptSet = new Set(kept); + const containsKept = (outer: ts.Node): boolean => { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + for (const c of childrenOf(n)) { + if (keptSet.has(c)) { + found = true; + return; + } + visit(c); + } + }; + visit(outer); + return found; + }; + for (const e of kept) { + if (!ts.isAwaitExpression(e) && containsKept(e) && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))) { + // `a.b.c` with `a.b` wrapped: the outer access reads typed data. + const recv = receiverOf(e); + if (recv !== null && (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv))) + continue; + } + const alias = exprCandidates.get(e)!; + const start = ts.tokenStartOfNode(e); + const stmt = e.parent; + let prefix = "("; + // A statement that would start with `(` after a line without a + // semicolon continues the previous expression: guard it. + let up: ts.Node | undefined = e; + while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; + if (up?.parent !== undefined && ts.isExpressionStatement(up.parent) && ts.tokenStartOfNode(up.parent) === start) + prefix = ";("; + void stmt; + fileEdits.push({ start, end: start, text: prefix }); + // Closing parts at one position close inner wraps first. + fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start }); + plan.push(`${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`); + } + + if (fileEdits.length === 0) continue; + edits.set(file, fileEdits); + } + + if (edits.size === 0) return null; + const files = new Map(); + for (const [file, list] of edits) { + const sf = input.jsProgram.getSourceFile(file)!; + files.set(file, applyEdits(sf.text, list)); + } + const declFile = `${input.outDir.split("\\").join("/")}/${DECL_BOUNDARY_FILE}`; + const declText = + "// Generated by scriptc: types projected from npm package declarations.\n" + + printer.declarations.join("\n") + + "\n"; + return { files, declFile, declText, plan }; +} + +/** Applies insertions and replacements; overlapping replacements keep the + * first. Insertions at one position keep their order. */ +export function applyEdits(text: string, edits: readonly Edit[]): string { + const sorted = edits + .map((e, i) => ({ ...e, i })) + .sort( + (a, b) => a.start - b.start || a.end - b.end || (a.rank ?? 0) - (b.rank ?? 0) || a.i - b.i, + ); + let out = ""; + let at = 0; + for (const e of sorted) { + if (e.start < at) continue; // overlaps an earlier replacement + out += text.slice(at, e.start) + e.text; + at = e.end; + } + return out + text.slice(at); +} diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index c3b4ad906..58992da54 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -288,6 +288,10 @@ export function applyWidthLift( case "dynIn": { if (dst.kind !== "dyn") throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot"); + // The declaration-typed boundary keeps user values' identity when + // they cross into package code (decl-projection.ts). + if (lowerer.declBoundary && !/[\\/]node_modules[\\/]/.test(loc.file) && liveBoxable(lowerer, value.type)) + return { kind: "dynFrom", value, liveRef: true, type: DYN, loc }; return { kind: "dynFrom", value, type: DYN, loc }; } case "dynOut": @@ -775,3 +779,16 @@ function buildRecordProjection( loc, }; } + +/** Records and arrays (and unions holding them), whose identity a live + * capsule keeps. */ +function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set()): boolean { + if (t.kind === "record") { + const shape = lowerer.shapes.get(t.shapeId); + return shape !== undefined && !shape.tuple; + } + if (t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index acac99610..d0064e56c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -13,6 +13,7 @@ import { STRING, type SrcLoc, UNDEFINED_T, + VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, @@ -47,6 +48,7 @@ import { import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js"; import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js"; import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js"; +import { isDeclBoundaryTypeNode } from "../../decl-projection.js"; /** `c ? a : b`. An explicit destination constructs each branch at that * layout before joining it. Without one, retain the expression's inferred @@ -1516,6 +1518,12 @@ export function lowerAsExpression( const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs); if (!target) lowerer.badType(expr.type, targetTs); if (target.kind === "dyn") return inner; // `as unknown`: erasure + // The declaration-typed boundary (decl-projection.ts): an identity- + // preserving checked conversion of a value package code produced. + if (isDeclBoundaryTypeNode(expr.type)) { + const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr)); + if (adopted) return adopted; + } // An all-`unknown`-fields record target (`err as { code?: unknown }` // — the errno-probing idiom): there is nothing to validate (every // field is unknown, exactly what the dyn value already answers) and @@ -1603,3 +1611,69 @@ export function lowerAsExpression( } return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) }; } + +/** True when a type holds a record or an array somewhere (values whose + * identity can be observed). */ +function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): boolean { + if (t.kind === "record" || t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false; +} + +/** `( as __scriptc_decl_N)`: check the dynamic value against + * the projected type and build the native value (dynCheck), then adopt: + * every source object or array that something else still references + * becomes a live capsule of the value built from it + * (scr_dyn_decl_adopt), so identity and mutation visibility hold on both + * sides. Scalar targets are ordinary checked casts. */ +function declBoundaryCast( + lowerer: Lowerer, + inner: IrExpr, + target: IrType, + loc: SrcLoc, +): IrExpr | null { + if ( + !canDynCheckTo( + target, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) && + !(target.kind === "union" && lowerer.dynConvertible(target)) + ) + return null; + if (!holdsReference(lowerer, target)) { + return { kind: "dynCheck", value: inner, type: target, loc }; + } + const source = lowerer.declareHiddenLocal("%boundarySource", DYN); + const value = lowerer.declareHiddenLocal("%boundaryValue", target); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: source.id, init: inner, loc }, + { + kind: "varDecl", + localId: value.id, + init: { kind: "dynCheck", value: varRef(source.id, DYN, loc), adopt: true, type: target, loc }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.declAdopt", + args: [ + varRef(source.id, DYN, loc), + { kind: "dynFrom", value: varRef(value.id, target, loc), liveRef: true, type: DYN, loc }, + ], + type: VOID, + loc, + }, + loc, + }, + ], + result: varRef(value.id, target, loc), + type: target, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 4adfb87ab..df4e747d6 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -227,6 +227,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const opaqueSeen = new Set(); const runtimeShapes = new Set(); const runtimeSeen = new Set(); + /** Shapes the declaration-typed boundary converts into (adopting casts): + * their values come from package code that builds them in the + * declaration's order, the order their table holds. */ + const declaredShapes = new Set(); + const declaredSeen = new Set(); const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); else shapesIn(type, runtimeShapes, runtimeSeen); @@ -332,7 +337,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio producedBy(e.type, true); return; case "dynCheck": - if (!orderPreservingClone(e)) producedBy(e.type, false); + if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); + else if (!orderPreservingClone(e)) producedBy(e.type, false); return; case "dynDestrCheck": case "caughtCheck": @@ -392,6 +398,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio f.opaque = true; } for (const id of runtimeShapes) factsOf(id).runtime = true; + for (const id of declaredShapes) if (!runtimeShapes.has(id)) factsOf(id).pinned = true; // ── scope ─────────────────────────────────────────────────────────── // Only plain declared-field shapes with a table take part. diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..c1b9a0ff8 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,4 @@ +import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; @@ -3565,6 +3566,18 @@ export class Lowerer { else if (kind === "failed") s.failed += 1; } + /** The program holds the declaration-typed boundary's projection + * (decl-projection.ts): user-file record and array values crossing into + * dynamic slots (package parameters) box as live capsules, so a value + * that comes back through a boundary cast keeps its identity. */ + private declBoundaryMemo: boolean | null = null; + get declBoundary(): boolean { + this.declBoundaryMemo ??= this.program + .getSourceFiles() + .some((sf) => isDeclBoundaryFile(sf.fileName)); + return this.declBoundaryMemo; + } + constructor( readonly program: ts.Program, readonly entry: ts.SourceFile, @@ -3687,8 +3700,11 @@ export class Lowerer { classImplementedProtocol: (type) => this.classImplementedProtocol(type), isStdlibFile: this.isStdlibFile, isNpmFile: this.isNpmFile, + // The declaration-typed boundary's generated types (decl-projection.ts) + // describe data, like an explicitly mapped external type surface. isExternalTypeFile: (sf) => - this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))), + this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))) || + isDeclBoundaryFile(sf.fileName), typeMemo: this.typeMemo, ...(this.generalNodeStreams ? { generalNodeStreams: true } : {}), ...(this.dynNodeStreams ? { dynNodeStreams: true } : {}), @@ -7708,6 +7724,7 @@ export class Lowerer { entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), + ...(this.declBoundary ? { declBoundary: true as const } : {}), ...(this.threadSlotKeys.size > 0 ? { threadSlotKeys: [...this.threadSlotKeys].sort() } : {}), diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 507d3392d..ef5e46d50 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -139,6 +139,7 @@ import { typeOnlyReachedFiles, } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; +import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -397,6 +398,9 @@ export interface LoadResult { /** The program's pre-emit diagnostics, computed once. */ diagnostics: readonly ts.Diagnostic[]; projectWorld: ts.Program | null; + /** The declaration-typed boundary rewrote user files: errors fall + * back to the declaration world (load.projectWorld()), as without it. */ + declWorldFallback?: true; }; /** How scriptc's type world differs from the project's own tsc setup — * used only to label the TypeScript errors those differences cause. */ @@ -846,6 +850,9 @@ function loadProgram7( externalTypes: ReadonlyMap = new Map(), /** The --npm-static lowering world: root opted-in packages' deep edges. */ rootNpmStaticEdges = false, + /** The declaration-typed boundary's projection (decl-projection.ts): + * answers rewritten user files for the lowering world, or null. */ + projector?: (program: ts.Program) => DeclProjection | null, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; @@ -940,6 +947,53 @@ function loadProgram7( }; for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text); program = host.createProgram([...programRoots, overridesDtsPath()], options); + } else if (projector !== undefined) { + // The declaration-typed boundary: user files typed from package + // declarations (decl-projection.ts). The tsc gate reads the original + // text; the lowering compiles the rewritten files. + const projection = projector(program); + if (projection !== null) { + const original = program; + const baseline = new Map(); + for (const file of projection.files.keys()) { + const sf = original.getSourceFile(file); + if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length); + } + for (const [file, text] of projection.files) host.addVirtualFile(file, text); + host.addVirtualFile(projection.declFile, projection.declText); + programRoots.push(projection.declFile); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + // A rewrite the lowering world disagrees with (new errors) is + // dropped for that file: the file lowers as it would without it. + const dropped: string[] = []; + for (const file of projection.files.keys()) { + const sf = program.getSourceFile(file); + if (sf === undefined) continue; + if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) { + dropped.push(file); + host.addVirtualFile(file, original.getSourceFile(file)!.text); + } + } + if (dropped.length > 0) { + program.dispose(); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + for (const file of dropped) { + config.diags.push(declBoundaryDroppedDiag(file)); + } + } + if (dropped.length === projection.files.size) { + program.dispose(); + programRoots.pop(); + program = original; + } else { + tscWorld = { + program: original, + diagnostics: ts.getPreEmitDiagnostics(original), + projectWorld: null, + declWorldFallback: true, + }; + } + } } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); @@ -1156,31 +1210,69 @@ export function loadProgram( (npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null, }; const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow }); + let authoredHost: ts.Ts7Host | null = null; + let authoredLoad: ReturnType | null = null; + const authored = (): ReturnType => { + if (!authoredLoad) { + authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); + authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); + } + return authoredLoad; + }; + const declPackages = npmStaticPackages.filter((pkg) => { + const resolved = resolveBareModule(entryPath, pkg, "types-only"); + return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile); + }); + const projector = + declPackages.length === 0 + ? undefined + : (program: ts.Program): DeclProjection | null => { + const userFiles = declBoundaryUserFiles(program, declPackages); + if (userFiles.length === 0) return null; + let projection: DeclProjection | null; + try { + projection = planDeclProjection({ + jsProgram: program, + declProgram: authored().program, + userFiles, + outDir: dirname(entryPath), + }); + } catch (error) { + if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error; + return null; + } + if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") { + process.stderr.write( + `[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`, + ); + if (projection !== null) process.stderr.write(projection.declText); + } + return projection; + }; try { - const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0); - let authoredHost: ts.Ts7Host | null = null; - let authoredLoad: ReturnType | null = null; + const load = loadProgram7( + host, + entryPath, + externalTypes, + npmStaticPackages.length > 0, + projector, + ); return { ...load, services, projectWorld: npmStaticPackages.length === 0 ? load.projectWorld - : () => { + : () => // Check authored code against the package's published declarations. // Runtime bodies still lower from JavaScript in the original world; // inference there must not manufacture errors in valid user code. - if (!authoredLoad) { - authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); - authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); - } - return authoredLoad.projectWorld(); - }, + authored().projectWorld(), dispose: () => { try { - authoredLoad?.disposeAll(); + (authoredLoad as ReturnType | null)?.disposeAll(); } finally { - authoredHost?.close(); + (authoredHost as ts.Ts7Host | null)?.close(); try { load.disposeAll(); } finally { @@ -1190,11 +1282,39 @@ export function loadProgram( }, }; } catch (error) { + (authoredLoad as ReturnType | null)?.disposeAll(); + (authoredHost as ts.Ts7Host | null)?.close(); host.close(); throw error; } } +/** User TypeScript files (outside node_modules) that import one of the + * packages whose declarations the boundary projects from. */ +function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] { + const out: string[] = []; + for (const sf of program.getSourceFiles()) { + const name = sf.fileName.split("\\").join("/"); + if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) + continue; + if (name.endsWith(".tsx")) continue; // JSX files: not projected yet + const text = sf.text; + if (packages.some((pkg) => text.includes(`"${pkg}"`) || text.includes(`'${pkg}'`) || text.includes(`"${pkg}/`) || text.includes(`'${pkg}/`))) + out.push(sf.fileName); + } + return out; +} + +/** SC6102 — a projected file the lowering world disagreed with. */ +function declBoundaryDroppedDiag(file: string): ScrDiagnostic { + return { + code: "SC6102", + message: + "values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript", + loc: { file, start: 0, end: 0 }, + }; +} + /** tsc diagnostics (syntax + types), the supported-import fence, and the * module evaluation order: fills load.moduleOrder (the SourceFiles * themselves — the lowering consumes them directly) and returns the @@ -4801,7 +4921,9 @@ function preflight7(load: LoadResult): { tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics); if (tscErrors.length > 0) { const world = - tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program); + tscWorld === undefined || tscWorld.declWorldFallback === true + ? load.projectWorld() + : (tscWorld.projectWorld ?? tscWorld.program); for (const d of errorsOf(world)) { const diag = toPassthrough(d); const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 3dc6dcebd..2d5e87ffb 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -610,6 +610,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // fence; the zero-argument form always throws ERR_MISSING_ARGS. The // DOMException clone shares the option validation. "dyn.structuredClone", + "dyn.declAdopt", "dyn.cloneMissing", "dyn.cloneTransferFail", "dyn.cloneTransferFailWith", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..d4b2cf2d2 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3776,6 +3776,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's * own (verbatim, doubly-wrapped) message. */ "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, + /** The declaration-typed boundary's adoption (decl-projection.ts): + * `source` is the dynamic value a boundary cast just converted, and + * `capsule` a live capsule of the native value built from it. Every + * object or array of `source` that something else still references + * becomes, in place, a live capsule of the native value built from it. + * Throws the path TypeError, changing nothing, when a converted object + * is not plain data (a prototype, accessors, symbols, frozen state). */ + "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, "dyn.cloneMissing": { argTypes: [], result: DYN }, /** structuredClone with a NON-EMPTY transfer array of static values: * nothing static is transferable, so the call always throws Node's diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 12400fee9..067f79a26 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1044,6 +1044,10 @@ export interface IrModule { entry: string; /** Executable roots run in isolated native worker contexts. */ workers?: boolean; + /** The program has declaration-typed boundary casts (decl-projection.ts): + * closures boxed for package code return records and arrays as live + * capsules, so a value that comes back keeps its identity. */ + declBoundary?: true; /** The program constructs LifetimeRegion (@scriptc/experimental): every * ProgramLifetime instance lives in a counted region (the region.* * intrinsics; backend/llvm/regions.ts). */ @@ -3879,10 +3883,19 @@ export type IrExpr = /** `exact`: the value is known to be a capsule this program boxed from * exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the * expression), so the check is one load of the capsule's instance. */ + /** `adopt`: the declaration-typed boundary's cast (decl-projection.ts). + * The conversion preserves identity: a source object or array that + * something else still references (reference count above one) is + * turned, after the whole check succeeded, into a live capsule of the + * value built from it, so every holder sees the same native value; an + * unshared source is simply consumed. Values must be plain data + * (ordinary objects and arrays without accessors, prototypes, symbols + * or frozen state), otherwise the cast throws the path TypeError. */ | { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; + adopt?: true; exact?: true; type: IrType; loc: SrcLoc; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 63aa8ff5c..adab9628f 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -120,6 +120,8 @@ export function validateModule(mod: IrModule): IrValidationError[] { const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 }; if (mod.workers !== undefined && typeof mod.workers !== "boolean") errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc }); + if (mod.declBoundary !== undefined && mod.declBoundary !== true) + errors.push({ message: "declBoundary must be true when present", loc: moduleLoc }); if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true) errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc }); if (mod.workers && mod.lib !== undefined) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..987de7758 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1000,6 +1000,11 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); * retained view released with the record. Its capsules adopt that view, and * the typed serializers consult `has` to serialize through the capsule. * `apply` consumes a fresh copying snapshot and returns it merged (+1). */ +/* The declaration-typed boundary's adoption (scr_json.c): every object or + * array of `source` that something else still references becomes a live + * capsule of the native value `capsule` built from it. Throws the path + * TypeError, changing nothing, when a converted node is not plain data. */ +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); 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); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..336d42e90 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15755,3 +15755,210 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) { return true; return scr_dyn_object_proto_name(key); } + +/* ── declaration-typed boundary: adoption ─────────────────────────────── + * A boundary cast (decl-projection.ts in the compiler) checks a value + * package code produced against the type the package's declarations give + * it and builds the native value. Copying would be observable whenever + * something else still references a source object or array, so this pass + * walks the source next to a live capsule of the built value and turns + * every source node with another holder (rc above one) into a live + * capsule of the native value built from it: every holder then reads and + * writes the same native value. Unshared nodes are left to be released + * with their parent, which no program can tell apart from a move. + * + * Phase one checks that every converted node is plain data (the native + * value has no prototype, accessors, symbol keys or frozen state) and + * collects the shared ones; it throws the path TypeError before anything + * changes. Phase two converts the shared nodes in place. Programs without + * boundary casts never reference this code (the linker drops it). */ +typedef struct { + ScrDyn *node; + ScrDyn *capsule; /* owned */ +} ScrDeclAdopt; + +typedef struct { + ScrDeclAdopt *items; + size_t len, cap; +} ScrDeclAdoptList; + +static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { + if (list->len == list->cap) { + list->cap = list->cap ? list->cap * 2 : 8; + list->items = realloc(list->items, list->cap * sizeof *list->items); + if (!list->items) scr_trap("scriptc: out of memory\n"); + } + list->items[list->len].node = node; + list->items[list->len].capsule = capsule; + list->len++; +} + +static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { + const char *want = NULL; + if (n->kind == SCR_DYN_OBJ) { + if (n->prototype || n->null_proto || n->v.obj.class_name) + want = "a plain object (this value has a prototype)"; + else if (n->symbol_properties || n->symbol_keys) + want = "a plain object (this value has symbol-keyed properties)"; + else if (n->non_extensible) + want = "a plain object (this value is frozen, sealed or not extensible)"; + else if (n->v.obj.source_access && !n->v.obj.source_identity) + want = "a plain object"; + else { + for (size_t i = 0; i < n->v.obj.len && !want; i++) { + const ScrDynEntry *e = &n->v.obj.entries[i]; + if (e->accessor) want = "a plain object (this value has accessor properties)"; + else if (!e->writable || !e->enumerable || !e->configurable) + want = "a plain object (this value has non-default property attributes)"; + } + } + } else if (n->kind == SCR_DYN_ARR) { + if (n->prototype) + want = "a plain array (this value has a prototype)"; + else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) + want = "a plain array (this value has extra properties)"; + else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) + want = "a plain array (this value is frozen, sealed or not extensible)"; + } + if (!want) return true; + scr_dyn_check_fail(path, want, n); + return false; +} + +static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { + for (size_t i = 0; i < list->len; i++) + if (list->items[i].node == node) return list->items[i].capsule; + return NULL; +} + +/* `capsule` is owned by the call: pushed or released. */ +static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, + const ScrDynPath *path, unsigned depth) { + if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || + capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { + scr_dyn_release(capsule); + return true; + } + if (!scr_decl_adopt_plain(n, path)) { + scr_dyn_release(capsule); + return false; + } + if (n->rc > 1 && n->rc != SIZE_MAX) { + ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); + if (seen) { + bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; + scr_dyn_release(capsule); + if (!same) + scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); + return same; + } + scr_decl_adopt_push(list, n, capsule); + } + void *ptr = capsule->v.typed_ref.ptr; + if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; + for (size_t i = 0; i < ops->count; i++) { + if (!ops->has(ptr, i)) continue; + const ScrStr *name = ops->names[i]; + ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, name->data, 0 }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && + capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; + for (size_t i = 0; i < n->v.arr.len; i++) { + ScrDyn *child = n->v.arr.items[i]; + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return true; +} + +/* Weak collections key a plain object by its node and a capsule by its + * native identity: move the node's entries to the identity it now has. */ +static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) { + bool moved = false; + if (!scr_weak_observing) return false; + for (;;) { + ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)]; + while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next; + ScrWeakEntry *entry = *slot; + if (!entry) return moved; + *slot = entry->all_next; + entry->key = ptr; + entry->kind = 32; + ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)]; + entry->all_next = *head; + *head = entry; + moved = true; + } +} + +static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) { + ScrDynRegion *region = scr_dyn_region_of(n); + if (region) region->intact = false; + /* The old payload moves to a fresh node, which is released below. A + * misc node carries no buffers of an earlier life. */ + ScrDyn *old = scr_dyn_alloc(SCR_DYN_NULL); + old->kind = n->kind; + old->buffer = n->buffer; + old->null_proto = n->null_proto; + old->copied_from_native = n->copied_from_native; + old->non_extensible = n->non_extensible; + old->v = n->v; + old->prototype = n->prototype; + old->symbol_properties = n->symbol_properties; + old->symbol_keys = n->symbol_keys; + n->kind = SCR_DYN_TYPED_REF; + n->buffer = false; + n->null_proto = false; + n->copied_from_native = false; + n->non_extensible = false; + n->prototype = NULL; + n->symbol_properties = NULL; + n->symbol_keys = NULL; + memset(&n->v, 0, sizeof n->v); + n->v.typed_ref.ptr = capsule->v.typed_ref.retain(capsule->v.typed_ref.ptr); + n->v.typed_ref.retain = capsule->v.typed_ref.retain; + n->v.typed_ref.release = capsule->v.typed_ref.release; + n->v.typed_ref.type_key = capsule->v.typed_ref.type_key; + n->v.typed_ref.type_key_len = capsule->v.typed_ref.type_key_len; + n->v.typed_ref.materialize = capsule->v.typed_ref.materialize; + n->v.typed_ref.commit = capsule->v.typed_ref.commit; + n->v.typed_ref.array = capsule->v.typed_ref.array; + n->v.typed_ref.record = capsule->v.typed_ref.record; + n->v.typed_ref.klass = capsule->v.typed_ref.klass; + n->v.typed_ref.traced = capsule->v.typed_ref.traced; + n->v.typed_ref.observed = capsule->v.typed_ref.observed; + if (scr_decl_adopt_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; + /* A record with undeclared properties answers them from its retained + * view, as a freshly boxed capsule does. */ + if (scr_record_source_count && n->v.typed_ref.type_key_len >= 7 && + memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { + ScrDyn *view = scr_record_source_view(n->v.typed_ref.ptr); + if (view) n->v.typed_ref.materialized = scr_dyn_retain(view); + } + scr_dyn_release(old); +} + +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { + ScrDeclAdoptList list = { NULL, 0, 0 }; + bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); + for (size_t i = 0; i < list.len; i++) { + ScrDeclAdopt *item = &list.items[i]; + if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_decl_adopt_become(item->node, item->capsule); + scr_dyn_release(item->capsule); + } + free(list.items); +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/main.ts b/tests/fixtures/npm/cases/zod3-boundary/main.ts new file mode 100644 index 000000000..8e8cedee7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/main.ts @@ -0,0 +1,83 @@ +// Values that cross from zod 3.25 (compiled from its JavaScript) into code +// typed by zod's declarations: identity, aliasing and mutation after parse, +// values zod hands back unchanged, callbacks zod calls, typed array work, +// and failing checks. +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([]), +}); +type Item = z.infer; +const items: Item[] = z.array(Item).parse([ + { sku: "a", price: 2.5, qty: 4, tags: ["x"] }, + { sku: "b", price: 10, qty: 1 }, + { sku: "c", price: 0.5, qty: 10, tags: ["x", "y"] }, +]); +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(",")); + +// 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]); + +// 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 passthrough 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 Loose = z.object({ id: z.number() }).passthrough(); +const loose = Loose.parse({ extra: true, id: 3, more: "m" }); +console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose)); + +// async parse +async function main(): Promise { + const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2 }); + console.log("async", it.sku, it.price * it.qty, it.tags.length); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/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/cases/zod3-boundary/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @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. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * 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) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * 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(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +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 const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +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"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + 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/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -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/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -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/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +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/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +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/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -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/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -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/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -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/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +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> { + private _cached; + _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/cases/zod3-boundary/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/package.json b/tests/fixtures/npm/cases/zod3-boundary/package.json new file mode 100644 index 000000000..be0bf47db --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/package.json @@ -0,0 +1 @@ +{ "name": "zod3-boundary-case", "private": true, "type": "module" } From f3bb5d2cb18fcd9bae91ccdd626a98472e5c022d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:57:07 -0500 Subject: [PATCH 083/169] Borrow the array of an inline Atomics operation from its local emitInlineAtomicsOp read its typed array with emitExpr, so every Atomics call on a local array retained the array before the atomic instruction and released it after, two out-of-line calls around one instruction. The array now goes through emitBorrowedInput: an unchanged, unboxed local owns it across the later operands and the operation, and the instruction and scr_atomics_op only read it. Fields, globals, captures and bindings an operand can reassign keep their own reference. --- .../compiler/src/backend/llvm/expr-bytes.ts | 4 ++- tests/harness/atomics-emission.test.ts | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index c16ae856a..41ddf2b9d 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -1118,7 +1118,9 @@ export function emitInlineAtomicsOp( if (op !== 0 && op !== 1 && op !== 3 && ATOMIC_RMW[op] === undefined) return null; const elem = array.type.elem; const B = host.B; - const receiver = host.emitExpr(array).name; + // The instruction and scr_atomics_op borrow the array: an unchanged local + // owns it across the later operands and the operation. + const receiver = emitBorrowedInput(host, array).name; const idxValue = host.emitExpr(index); const operand = host.emitExpr(value); const swap = host.emitExpr(replacement); diff --git a/tests/harness/atomics-emission.test.ts b/tests/harness/atomics-emission.test.ts index 9ae06d904..2d708e382 100644 --- a/tests/harness/atomics-emission.test.ts +++ b/tests/harness/atomics-emission.test.ts @@ -61,3 +61,32 @@ console.log(work(new Int32Array(buffer), new Uint8Array(buffer), new Int16Array( expect(calls).toHaveLength(9); for (const call of calls) expect(call).toMatch(/\) cold$/); }); + +test("an unchanged local array is borrowed across Atomics operations", async () => { + const llvm = await emittedLlvm( + "borrow", + `function count(buffer: SharedArrayBuffer, n: number): number { + const cells = new Int32Array(buffer); + for (let i = 0; i < n; i++) { + Atomics.add(cells, 0, 1); + Atomics.compareExchange(cells, 1, Atomics.load(cells, 1), i); + } + return Atomics.load(cells, 0); +} +function swap(buffer: SharedArrayBuffer, other: SharedArrayBuffer): number { + let cells = new Int32Array(buffer); + return Atomics.add(cells, ((cells = new Int32Array(other)), 0), 1); +} +const buffer = new SharedArrayBuffer(16); +console.log(count(buffer, 3), swap(buffer, new SharedArrayBuffer(16))); +`, + ); + // The local owns the array across each operation: no reference count + // operations around the atomic instructions. + const counted = body(llvm, "sc_bf_count"); + expect(counted).toMatch(/atomicrmw add ptr/); + expect(counted).not.toMatch(/call ptr @scr_bytes_retain_v/); + // An operand that reassigns the binding releases the old array, so the + // operation keeps its own reference. + expect(body(llvm, "sc_bf_swap")).toMatch(/call ptr @scr_bytes_retain_v/); +}); From 7f7e11a1a749beb0f14d340fdb193e99e013c18b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:21:31 -0700 Subject: [PATCH 084/169] Read and write Error.stackTraceLimit through the Error constructor value zod 4 saves and restores the limit through an alias, `const E = "captureStackTrace" in Error ? Error : null`. The interned Error constructor value now answers `stackTraceLimit` from the same limit the static member and stack capture use, and numeric writes set it, so `E.stackTraceLimit` reads 10 instead of undefined and `E.stackTraceLimit = 0` reaches `Error.stackTraceLimit`. Co-Authored-By: Claude Opus 5.5 --- .../7602-error-stack-limit-value.js.json | 6 ++++++ packages/runtime/src/scr_json.c | 13 +++++++++++++ tests/corpus/7602-error-stack-limit-value.js | 18 ++++++++++++++++++ .../7602-error-stack-limit-value.js.json | 1 + 4 files changed, 38 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json create mode 100644 tests/corpus/7602-error-stack-limit-value.js create mode 100644 tests/harness/corpus-costs/7602-error-stack-limit-value.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json new file mode 100644 index 000000000..99e1b8904 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7602-error-stack-limit-value.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..074fe3eae 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1870,6 +1870,14 @@ ScrDyn *scr_dyn_error_constructor(const char *name) { return NULL; } +/* `Error.stackTraceLimit` read or written through the Error constructor + * held as a value (zod's `const E = Error; E.stackTraceLimit = 0`): the + * same limit the static member reads and stack capture honors. */ +static bool scr_dyn_is_stack_limit(const ScrDyn *d, const char *key, size_t key_len) { + return key_len == 15 && memcmp(key, "stackTraceLimit", 15) == 0 && d->kind == SCR_DYN_FUNC && + scr_error_ctor_values[0] && d->v.fn.clo == scr_error_ctor_values[0]->v.fn.clo; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -8992,6 +9000,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; + if (value->kind == SCR_DYN_NUM && scr_dyn_is_stack_limit(recv, key->data, key->len)) { + scr_stack_limit_set(value->v.num); + return; + } ScrDyn *table = scr_dyn_fn_properties(recv); scr_dyn_object_key_set(table, key, value, recv); scr_dyn_release(table); @@ -11773,6 +11785,7 @@ static ScrDyn *scr_function_constructor(uint32_t kind) { /* Keyed read on a FUNC node (see scr_runtime.h): own props first, then * the function-instance built-ins name/length. +1 or NULL. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { + if (scr_dyn_is_stack_limit(d, key, key_len)) return scr_dyn_new_num(scr_stack_limit_get()); ScrClosure *owner = scr_closure_identity(d->v.fn.clo); if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props) owner->props = scr_box_retain(d->v.fn.class_obj->static_data); diff --git a/tests/corpus/7602-error-stack-limit-value.js b/tests/corpus/7602-error-stack-limit-value.js new file mode 100644 index 000000000..4b3f8cc7e --- /dev/null +++ b/tests/corpus/7602-error-stack-limit-value.js @@ -0,0 +1,18 @@ +// Error.stackTraceLimit through the Error constructor held as a value +// (zod 4's `const E = "captureStackTrace" in Error ? Error : null`): reads +// and writes reach the same limit as the static member. +const E = "captureStackTrace" in Error ? Error : null; +console.log(typeof E, E === Error, E.stackTraceLimit, Error.stackTraceLimit); +const saved = E.stackTraceLimit; +E.stackTraceLimit = 0; +console.log(Error.stackTraceLimit, E.stackTraceLimit, typeof saved === "number"); +console.log(new Error("no frames").stack); +try { + E.stackTraceLimit = 3; + console.log(Error.stackTraceLimit); +} finally { + E.stackTraceLimit = saved; +} +console.log(E.stackTraceLimit, Error.stackTraceLimit); +Error.stackTraceLimit = 4; +console.log(E.stackTraceLimit); diff --git a/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } From 3fff00e5f77027602ac913eced1cd9f1a55f0cf0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:21:31 -0700 Subject: [PATCH 085/169] Fold navigator member reads under optional chains zod 4 probes `navigator?.userAgent?.includes("Cloudflare")`. Node's navigator global is never nullish and has no value form here, so the optional step is the plain member read: `navigator?.userAgent` folds to the same `Node.js/` literal as `navigator.userAgent`, instead of lowering the navigator object as a value and refusing at run time. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/expressions/optional-chains.ts | 11 +++++++++++ .../frontend/lowering/expressions/property-access.ts | 8 ++++---- .../corpus/7600-navigator-optional-chain.js.json | 6 ++++++ tests/corpus/7600-navigator-optional-chain.js | 7 +++++++ .../7600-navigator-optional-chain.js.json | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json create mode 100644 tests/corpus/7600-navigator-optional-chain.js create mode 100644 tests/harness/corpus-costs/7600-navigator-optional-chain.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 4b8c5692a..104d85a80 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -1,3 +1,4 @@ +import { isNavigatorGlobal } from "../builtins/navigator.js"; import { isSafeToDiscard } from "./evaluation-safety.js"; import { completeFuncValueArgs } from "../call-arguments.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; @@ -223,6 +224,16 @@ export function lowerOptionalChain( // Native handle methods have no first-class value to guard; when the // method read itself has no lowering, re-dispatch the plain call (which // reports its own diagnostics) instead of failing on the value read. + // `navigator?.userAgent`: Node's navigator global is never nullish and + // has no value form, so the guarded step is the plain member read. + if (isNavigatorGlobal(lowerer, recvNode)) { + lowerer.chainHandled.add(dotNode); + try { + return lowerer.lowerExpr(expr); + } finally { + lowerer.chainHandled.delete(dotNode); + } + } let loweredReceiver: IrExpr | null; if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) { loweredReceiver = tryLowerExpression(lowerer, recvNode); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 2e6ab981a..498038831 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -306,10 +306,10 @@ export function lowerPropertyAccessExpr( }; } } - if (!expr.questionDotToken) { - const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); - if (navigator) return navigator; - } + // Node's navigator is never nullish, so `navigator?.userAgent` (zod's + // Cloudflare probe) folds like `navigator.userAgent`. + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; // 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 diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json new file mode 100644 index 000000000..12465d5e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7600-navigator-optional-chain.js" + ], + "diags": [] +} diff --git a/tests/corpus/7600-navigator-optional-chain.js b/tests/corpus/7600-navigator-optional-chain.js new file mode 100644 index 000000000..5c1dac442 --- /dev/null +++ b/tests/corpus/7600-navigator-optional-chain.js @@ -0,0 +1,7 @@ +// Node's navigator global read through optional chains (zod 4's +// `navigator?.userAgent?.includes("Cloudflare")` probe): navigator is +// never nullish, so each chain answers like the plain member read. +const cloudflare = typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare"); +console.log(cloudflare); +console.log(navigator?.userAgent === navigator.userAgent, navigator?.userAgent?.startsWith("Node.js/")); +console.log(navigator?.userAgent?.split("/")[0] ?? "none"); diff --git a/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json b/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json new file mode 100644 index 000000000..e350cf2da --- /dev/null +++ b/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 22000 } From 3041df387d772511f0e8dbb04ca9aa4d20dd81f3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 17:43:57 -0700 Subject: [PATCH 086/169] Spread untyped iterables into Math.min and Math.max Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-stdlib.ts | 19 ++++++++++ packages/compiler/src/ir/builtin-effects.ts | 2 ++ .../compiler/src/ir/builtin-signatures.ts | 1 + .../corpus/7488-math-spread-untyped.js.json | 6 ++++ packages/runtime/src/scr_dyn.h | 3 ++ packages/runtime/src/scr_json.c | 35 +++++++++++++++++++ tests/corpus/7488-math-spread-untyped.js | 33 +++++++++++++++++ .../7488-math-spread-untyped.js.json | 1 + 9 files changed, 101 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json create mode 100644 tests/corpus/7488-math-spread-untyped.js create mode 100644 tests/harness/corpus-costs/7488-math-spread-untyped.js.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..488b81d57 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -973,6 +973,7 @@ export const LIB_FN_SYMS: Record = { "dyn.typedRefIs": "scr_dyn_typed_ref_is_key", "dyn.classIs": "scr_dyn_class_is_key", "dyn.iterPack": "scr_dyn_iter_pack", + "dyn.mathFold": "scr_dyn_math_fold", "dyn.destructureRest": "scr_dyn_destructure_rest", "dyn.iterator": "scr_dyn_iterator", "dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open", diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 0beb24a0e..e671a0a08 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2496,6 +2496,25 @@ export function lowerStdlibMethodCall( // method, drains into the number[] the fold reads. src = classGeneratorIterable(lowerer, src) ?? src; if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src; + // An untyped iterable (`Math.min(...caps.map(f))` in JavaScript): the + // runtime packs its values and folds their ToNumber like Node. V8 names + // a null or undefined source by its text, calls as `f(...)`. + if (src.type.kind === "dyn") { + const what = ts.isCallExpression(spread.expression) + ? `${ts.getTextOfNode(spread.expression.expression)}(...)` + : ts.getTextOfNode(spread.expression); + return { + kind: "libCall", + fn: "dyn.mathFold", + args: [ + src, + { kind: "boolLit", value: name === "max", type: BOOL, loc }, + { kind: "strLit", value: what, type: STRING, loc }, + ], + type: F64, + loc, + }; + } if (src.type.kind !== "array" || src.type.elem.kind !== "f64") { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 3dc6dcebd..d8e78be08 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -671,6 +671,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.propertyIsEnumerableComputed", // the destructuring pack throws V8's TypeError on non-iterable dyn kinds "dyn.iterPack", + // ToNumber of each element can run valueOf + "dyn.mathFold", "dyn.destructureRest", "dyn.arrayFromIterator", "dyn.iterator", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..9dbcc1514 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -219,6 +219,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * not iterable (cannot read property Symbol(Symbol.iterator))"). In the * may-throw seed set. */ "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, + "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json new file mode 100644 index 000000000..4d1376a12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7488-math-spread-untyped.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..100dd2295 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1273,6 +1273,9 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); +/* Math.max/Math.min(...src) over an untyped iterable; `what` names the + * source in V8's null/undefined spread TypeError. NaN with it pending. */ +double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what); /* JS String() over a dyn value (arrays join, [object Object], the error * encoding renders "name: message"); borrowed, result +1, never throws. */ ScrStr *scr_dyn_display(const ScrDyn *d); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..9abb7796b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1557,6 +1557,41 @@ static ScrDyn *scr_dyn_not_iterable(const ScrDyn *src, const ScrStr *msg) { return NULL; } +/* Math.max(...src) / Math.min(...src) over an untyped iterable: every + * value's ToNumber runs in order, then NaN poisons and ±0 orders as in + * Node. A non-iterable source throws V8's call-spread TypeError: `what` + * names the source for null and undefined, every other kind gets the + * Symbol.iterator wording. NaN with the exception pending when packing or + * a coercion throws. */ +double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what) { + if (src->kind == SCR_DYN_UNDEF || src->kind == SCR_DYN_NULL) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, what->data, what->len); + scr_jb_puts(&b, src->kind == SCR_DYN_NULL ? " is not iterable (cannot read property null)" + : " is not iterable (cannot read property undefined)"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NAN; + } + static const char spread_msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function"; + ScrStr *msg = scr_str_new(spread_msg, sizeof spread_msg - 1); + ScrDyn *pack = scr_dyn_iter_pack(src, msg); + scr_str_release(msg); + if (!pack) return NAN; + double best = max ? -INFINITY : INFINITY; + bool nan = false; + for (size_t i = 0; i < pack->v.arr.len; i++) { + double v = scr_dyn_number_coerce(pack->v.arr.items[i]); + if (scr_exc_pending()) break; + if (isnan(v)) nan = true; + else if (max ? (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) + : (v < best || (v == 0.0 && best == 0.0 && signbit(v)))) + best = v; + } + scr_dyn_release(pack); + return nan ? NAN : best; +} + ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { if (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) return scr_dyn_handle_ops_of(src)->iter_pack(src->v.handle.ptr); diff --git a/tests/corpus/7488-math-spread-untyped.js b/tests/corpus/7488-math-spread-untyped.js new file mode 100644 index 000000000..a30ece320 --- /dev/null +++ b/tests/corpus/7488-math-spread-untyped.js @@ -0,0 +1,33 @@ +// Math.max/Math.min spreading an untyped iterable (zod's +// `Math.min(...caps.map((c) => c[0]))`). +function caps(list) { + return list.map((c) => c[0]); +} +console.log(Math.min(...caps([[5], [2], [9]])), Math.max(...caps([[5], [2], [9]]))); +console.log(Math.min(...caps([])), Math.max(...caps([]))); +console.log(Math.min(...caps([["3"], [true], [null]])), Math.max(...caps([["7"], [undefined]]))); +console.log(Object.is(Math.max(...caps([[-0], [0]])), 0), Object.is(Math.min(...caps([[0], [-0]])), -0)); +const order = []; +const valued = (n) => ({ valueOf() { order.push(n); return n; } }); +console.log(Math.max(...caps([[valued(1)], [NaN], [valued(3)]])), order.join(",")); +try { + Math.min(...caps([[{ valueOf() { throw new Error("boom"); } }]])); +} catch (e) { + console.log(e.message); +} +const sources = { none: undefined, nil: null, num: 5, obj: {}, str: "48" }; +function pick(k) { + return sources[k]; +} +for (const k of Object.keys(sources)) { + try { + console.log(Math.max(...sources[k])); + } catch (e) { + console.log(e.constructor.name, e.message); + } + try { + console.log(Math.min(...pick(k))); + } catch (e) { + console.log(e.constructor.name, e.message); + } +} diff --git a/tests/harness/corpus-costs/7488-math-spread-untyped.js.json b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } From 4b34943b1ad00c229bf2656d21c61c94a36997ba Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:03:00 -0700 Subject: [PATCH 087/169] Adopt closure arguments, add adoption, @types and failing-check cases Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/dyn.ts | 20 + .../src/frontend/decl-projection.test.ts | 33 + .../fixtures/npm/cases/types-boundary/main.ts | 19 + .../node_modules/@types/rowparse/index.d.ts | 12 + .../node_modules/@types/rowparse/package.json | 1 + .../node_modules/rowparse/index.js | 26 + .../node_modules/rowparse/package.json | 1 + .../npm/cases/types-boundary/package.json | 1 + .../fixtures/npm/cases/zod3-adoption/main.ts | 39 + .../zod3-adoption/node_modules/zod/LICENSE | 21 + .../node_modules/zod/dist/esm/index.js | 3 + .../node_modules/zod/dist/esm/package.json | 3 + .../node_modules/zod/dist/esm/v3/ZodError.js | 132 + .../node_modules/zod/dist/esm/v3/errors.js | 9 + .../node_modules/zod/dist/esm/v3/external.js | 6 + .../zod/dist/esm/v3/helpers/enumUtil.js | 1 + .../zod/dist/esm/v3/helpers/errorUtil.js | 6 + .../zod/dist/esm/v3/helpers/parseUtil.js | 109 + .../zod/dist/esm/v3/helpers/partialUtil.js | 1 + .../zod/dist/esm/v3/helpers/typeAliases.js | 1 + .../zod/dist/esm/v3/helpers/util.js | 133 + .../node_modules/zod/dist/esm/v3/index.js | 4 + .../zod/dist/esm/v3/locales/en.js | 107 + .../zod/dist/esm/v3/standard-schema.js | 1 + .../node_modules/zod/dist/esm/v3/types.js | 3691 +++++++++++++++++ .../node_modules/zod/dist/types/index.d.ts | 3 + .../zod/dist/types/v3/ZodError.d.ts | 164 + .../zod/dist/types/v3/errors.d.ts | 5 + .../zod/dist/types/v3/external.d.ts | 6 + .../zod/dist/types/v3/helpers/enumUtil.d.ts | 8 + .../zod/dist/types/v3/helpers/errorUtil.d.ts | 9 + .../zod/dist/types/v3/helpers/parseUtil.d.ts | 78 + .../dist/types/v3/helpers/partialUtil.d.ts | 8 + .../dist/types/v3/helpers/typeAliases.d.ts | 2 + .../zod/dist/types/v3/helpers/util.d.ts | 85 + .../node_modules/zod/dist/types/v3/index.d.ts | 4 + .../zod/dist/types/v3/locales/en.d.ts | 3 + .../zod/dist/types/v3/standard-schema.d.ts | 102 + .../node_modules/zod/dist/types/v3/types.d.ts | 1031 +++++ .../node_modules/zod/package.json | 120 + .../npm/cases/zod3-adoption/package.json | 1 + .../fixtures/npm/cases/zod3-boundary/lying.ts | 19 + tests/harness/decl-boundary.test.ts | 33 + 43 files changed, 6061 insertions(+) create mode 100644 packages/compiler/src/frontend/decl-projection.test.ts create mode 100644 tests/fixtures/npm/cases/types-boundary/main.ts create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json create mode 100644 tests/fixtures/npm/cases/types-boundary/package.json create mode 100644 tests/fixtures/npm/cases/zod3-adoption/main.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/cases/zod3-adoption/package.json create mode 100644 tests/fixtures/npm/cases/zod3-boundary/lying.ts create mode 100644 tests/harness/decl-boundary.test.ts diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 72e6c6c9e..9ce8922d5 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -4684,6 +4684,26 @@ export class LlDyn { }, "ptr null", ); + // Declaration-typed boundary programs: an argument package code + // passes keeps its identity (scr_dyn_decl_adopt). + const capsule = this.host.declBoundary?.() === true ? this.liveToDyn(B, p, a) : null; + if (capsule !== null) { + host.declare(`declare void @scr_dyn_decl_adopt(ptr, ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + B.line(`call void @scr_dyn_decl_adopt(ptr ${ad}, ptr ${capsule})`); + B.line(`call void @scr_dyn_release(ptr ${capsule})`); + this.pendingBail( + B, + "dfk.adopt", + () => { + t.params.slice(0, i + 1).forEach((q, j) => { + const name = j === i ? a : argNames[j]; + if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${name})`); + }); + }, + "ptr null", + ); + } argNames.push(a); } }); diff --git a/packages/compiler/src/frontend/decl-projection.test.ts b/packages/compiler/src/frontend/decl-projection.test.ts new file mode 100644 index 000000000..ef8771b06 --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "vitest"; +import { applyEdits, isDeclBoundaryFile, isDeclBoundaryTypeName } from "./decl-projection.js"; + +describe("decl-projection", () => { + test("applyEdits nests wraps that close at one position", () => { + const text = "const p = a.b;"; + // Outer wrap of `a.b` and inner wrap of `a`... closing at different ends; + // two wraps closing at the same position close the inner one first. + const out = applyEdits(text, [ + { start: 10, end: 10, text: "(" }, + { start: 13, end: 13, text: " as Outer)", rank: -10 }, + { start: 10, end: 10, text: "(" }, + { start: 13, end: 13, text: " as Inner)", rank: -9 }, + ]); + expect(out).toBe("const p = ((a.b as Inner) as Outer);"); + }); + + test("applyEdits keeps the first of overlapping replacements and every line", () => { + const text = "type A = z.infer;\nconst x = 1;"; + const out = applyEdits(text, [ + { start: 9, end: 26, text: "__scriptc_decl_0" }, + { start: 11, end: 16, text: "nope" }, + ]); + expect(out).toBe("type A = __scriptc_decl_0;\nconst x = 1;"); + }); + + test("names", () => { + expect(isDeclBoundaryTypeName("__scriptc_decl_3")).toBe(true); + expect(isDeclBoundaryTypeName("Product")).toBe(false); + expect(isDeclBoundaryFile("/app/src/__scriptc_decl_boundary__.d.ts")).toBe(true); + expect(isDeclBoundaryFile("/app/src/types.d.ts")).toBe(false); + }); +}); diff --git a/tests/fixtures/npm/cases/types-boundary/main.ts b/tests/fixtures/npm/cases/types-boundary/main.ts new file mode 100644 index 000000000..c33e6e9bf --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/main.ts @@ -0,0 +1,19 @@ +// A plain-JavaScript package typed by an @types package: rows the +// declarations type as plain data become native records; the cursor, a +// constructor-function instance its declarations describe with an +// interface, keeps its JavaScript-world typing. +import { parseRows, cursor, type Row } from "rowparse"; + +const rows: Row[] = parseRows("name,qty\napple,3\npear,5\nfig,2\n"); +let total = 0; +for (const r of rows) total += Number(r.cells["qty"]); +console.log("total", total, rows.length); +const widest = rows.reduce((w, r) => Math.max(w, r.width), 0); +console.log("widest", widest); +rows[0]!.cells["qty"] = "30"; +console.log("first", JSON.stringify(rows[0]), Object.keys(rows[1]!.cells).join("|")); +const sorted = [...rows].sort((a, b) => a.cells["name"]!.localeCompare(b.cells["name"]!)).map((r) => r.line); +console.log("sorted", sorted.join(",")); +const c = cursor("abcdef"); +c.pos = 2; +console.log("cursor", c.remaining(), c.text); diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts new file mode 100644 index 000000000..8e099794d --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts @@ -0,0 +1,12 @@ +export type Row = { + line: number; + cells: { [column: string]: string }; + width: number; +}; +export declare function parseRows(text: string): Row[]; +export interface Cursor { + text: string; + pos: number; + remaining(): number; +} +export declare function cursor(text: string): Cursor; diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json new file mode 100644 index 000000000..7ce3bcc0e --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json @@ -0,0 +1 @@ +{ "name": "@types/rowparse", "version": "1.0.0", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js new file mode 100644 index 000000000..4a53c8481 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js @@ -0,0 +1,26 @@ +"use strict"; +// Plain JavaScript: rows are built key by key, so inference sees nothing. +function Cursor(text) { + this.text = text; + this.pos = 0; +} +Cursor.prototype.remaining = function () { + return this.text.length - this.pos; +}; +function parseRows(text) { + var lines = String(text).split("\n").filter(Boolean); + var header = lines[0].split(","); + var rows = []; + for (var i = 1; i < lines.length; i++) { + var cells = lines[i].split(","); + var row = {}; + for (var j = 0; j < header.length; j++) row[header[j]] = cells[j]; + rows.push({ line: i, cells: row, width: cells.length }); + } + return rows; +} +function cursor(text) { + return new Cursor(text); +} +exports.parseRows = parseRows; +exports.cursor = cursor; diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json new file mode 100644 index 000000000..da66597bc --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json @@ -0,0 +1 @@ +{ "name": "rowparse", "version": "1.0.0", "main": "index.js" } diff --git a/tests/fixtures/npm/cases/types-boundary/package.json b/tests/fixtures/npm/cases/types-boundary/package.json new file mode 100644 index 000000000..bae20e44c --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/package.json @@ -0,0 +1 @@ +{ "name": "types-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-adoption/main.ts b/tests/fixtures/npm/cases/zod3-adoption/main.ts new file mode 100644 index 000000000..6566d0e3a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/main.ts @@ -0,0 +1,39 @@ +// Adoption at the declaration-typed boundary: objects package code still +// references (a cache, weak keys, parsed documents, a cycle through an +// unknown field) keep their identity when they cross into typed code. +import { z } from "zod"; + +const Row = z.object({ id: z.number(), name: z.string(), tags: z.array(z.string()), meta: z.unknown() }); +type Row = z.infer; + +// A package-side cache: the same objects come back from every call. +const cache: unknown[] = []; +const weak = new WeakMap(); +const Cached = z.custom<{ id: number; tags: string[] }>().transform((v) => v); +for (let i = 0; i < 4; i++) { + const raw = JSON.parse(`{"id":${i},"name":"n${i}","tags":["a","b"],"meta":{"deep":[1,2,{"x":${i}}]}}`) as unknown; + cache.push(raw); + weak.set(raw as object, "w" + i); +} +let sum = 0; +for (let round = 0; round < 200; round++) { + for (let i = 0; i < cache.length; i++) { + const r: Row = Row.parse(cache[i]); + sum += r.id + r.tags.length; + const again = Row.parse(cache[i]); + if (round === 0) console.log("fresh", r === again, r.name, JSON.stringify(r.meta)); + } +} +console.log("sum", sum); +// The package keeps the source objects; z.custom passes them through. +const kept = cache.map((c) => Cached.parse(c)); +kept[0]!.tags.push("c"); +console.log("kept", JSON.stringify(cache[0]), kept[0] === Cached.parse(cache[0])); +console.log("weak", weak.get(cache[1] as object), weak.has(cache[2] as object)); +// A cycle through an unknown field. +const AsRow = z.custom(); +const cyc: Row = { id: 9, name: "c", tags: [], meta: null }; +cyc.meta = cyc; +const back = AsRow.parse(cyc); +back.id = 10; +console.log("cycle", cyc.id, back === cyc, back.meta === cyc); diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/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/cases/zod3-adoption/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @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. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * 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) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * 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(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +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 const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +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"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + 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/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -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/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -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/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +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/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +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/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -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/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -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/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -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/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +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> { + private _cached; + _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/cases/zod3-adoption/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/package.json b/tests/fixtures/npm/cases/zod3-adoption/package.json new file mode 100644 index 000000000..ff368ad87 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/package.json @@ -0,0 +1 @@ +{ "name": "zod3-adoption-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-boundary/lying.ts b/tests/fixtures/npm/cases/zod3-boundary/lying.ts new file mode 100644 index 000000000..107dda7d8 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/lying.ts @@ -0,0 +1,19 @@ +// A declaration that lies about a value: the boundary check throws a +// TypeError naming the path (native only; Node does not check). Run by +// tests/harness/decl-boundary.test.ts. +import { z } from "zod"; + +const Liar = z.custom<{ name: string; n: number }>(() => true); +try { + const v = Liar.parse({ name: "x", n: "not a number" }); + console.log("liar", v.n + 1); +} catch (e) { + console.log("liar threw", e instanceof TypeError, (e as Error).message); +} +const Frozen = z.custom<{ tags: string[] }>(); +try { + const f = Frozen.parse(Object.freeze(JSON.parse('{"tags":["a"]}'))); + console.log("frozen", f.tags.length); +} catch (e) { + console.log("frozen threw", (e as Error).message); +} diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts new file mode 100644 index 000000000..7f8e93c84 --- /dev/null +++ b/tests/harness/decl-boundary.test.ts @@ -0,0 +1,33 @@ +/* The declaration-typed boundary's checks (decl-projection.ts): a value + * zod's declarations type wrongly fails the boundary cast with a TypeError + * naming the path, and a value the native record cannot represent (a + * frozen object) fails with the reason. Node does not check, so this is a + * native-only test; npm.test.ts runs the differential cases + * (zod3-boundary, zod3-adoption, zod3-infer). */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/lying.ts"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/decl-boundary"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test("boundary casts throw a path TypeError for values the declarations misdescribe", { timeout: 600_000 }, async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + pending.child.stdin?.end(); + const { stdout } = await pending; + expect(stdout).toBe( + "liar threw true expected number at $.n, got string\n" + + "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", + ); +}); From 94173bebdc4480d661d5316e3ac96a6eb8374a33 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:12:16 -0700 Subject: [PATCH 088/169] Name the compiler's own shim files independently of the install path Programs that compile against a package shim (fs-extra, chalk, undici, hot-shots) produced IR and LLVM that depended on where the compiler was installed: lifted helper and closure names hashed the shim file's path relative to the entry, the shared-helper location pass ordered candidate sites by absolute path, and IR artifacts spelled every shim location as an absolute path. Two checkouts of the same sources therefore emitted different artifacts, and the zero-cost gate always reported fs-extra-package as differing. Files scriptc ships are now named by their place inside scriptc (scriptc:shims/...) in structural positions, in the shared-helper ordering, and in serialized IR artifacts. In-memory locations keep the real path, so diagnostics and source lookups are unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/dts-paths.ts | 25 +++++++++++++ .../compiler/src/frontend/lowering/lowerer.ts | 30 +++++++++++----- packages/compiler/src/index.ts | 9 ++--- packages/compiler/src/ir/serialize.test.ts | 21 +++++++++++ packages/compiler/src/ir/serialize.ts | 36 +++++++++++++++++-- packages/compiler/src/native/prepare.ts | 7 ++-- 6 files changed, 111 insertions(+), 17 deletions(-) diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts index 3b2f32413..009d1c333 100644 --- a/packages/compiler/src/frontend/dts-paths.ts +++ b/packages/compiler/src/frontend/dts-paths.ts @@ -34,6 +34,31 @@ export function packageShimRoot(): string { : join(installedDeclarationRoot, "shims"); } +/** The directories of files scriptc itself ships, each with the prefix + * that names its files independently of where scriptc is installed + * (compilerOwnedFileName). IR artifacts spell source locations in them this + * way (serializeModuleParts). */ +export function compilerOwnedFileRoots(): readonly { from: string; to: string }[] { + return [{ from: tsgoPath(packageShimRoot()) + "/", to: "scriptc:shims/" }]; +} + +/** The name of a file scriptc itself ships (a package shim or one of the + * shipped declaration files) that does not depend on where scriptc is + * installed: `scriptc:shims/` or + * `scriptc:`. Null for every other file. Names and + * artifacts that would otherwise spell the installation's absolute path use + * it, so two installations of one compiler emit the same output. */ +export function compilerOwnedFileName(file: string): string | null { + const path = tsgoPath(file); + for (const { from, to } of compilerOwnedFileRoots()) { + if (path.startsWith(from)) return to + path.slice(from.length); + } + for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) { + if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`; + } + return null; +} + /** Path of the shipped ambient declarations — the always-shipped CORE * (comptime, setTimeout). Part of EVERY program scriptc * builds, the project-world preflight program included. */ diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..60eba3107 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -184,7 +184,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; */ import { dirname, relative, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; -import { tsgoPath } from "../dts-paths.js"; +import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; import { isBundledShimForwarder } from "../package-shims.js"; import { erasedTypeOnlyImport } from "../type-only-imports.js"; @@ -3200,13 +3200,14 @@ export class Lowerer { /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ /** Files are named by their path relative to the entry's directory, so - * names do not depend on where the program sits, and default library - * files by their base name: two checkers install them in different - * directories. */ + * names do not depend on where the program sits, default library files + * by their base name (two checkers install them in different + * directories), and the files scriptc ships by their name inside scriptc, + * so names do not depend on where scriptc is installed either. */ readonly positions = new StructuralPositions((sf) => this.program.isSourceFileDefaultLibrary(sf) ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` - : relative(dirname(this.entry.fileName), sf.fileName), + : (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)), ); /** The outermost node whose token span is exactly `loc`, or null. */ @@ -3238,7 +3239,10 @@ export class Lowerer { * before it. */ closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { const node = this.nodeAtLoc(loc); - const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const where = + node === null + ? `${compilerOwnedFileName(loc.file) ?? loc.file}:${loc.start}:${loc.end}` + : this.positions.of(node); const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; const first = keyedHelperName(this, prefix, key); const seen = this.closureNameCounts.get(first) ?? 0; @@ -3379,8 +3383,18 @@ export class Lowerer { if (interned.has(fn.name)) this.sharedHelpers.add(fn); if (this.sharedHelpers.size === 0) return; const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn])); - const earlier = (left: SrcLoc, right: SrcLoc): boolean => - left.file < right.file || (left.file === right.file && left.start < right.start); + // Files scriptc ships compare by their name inside scriptc, so the + // choice does not depend on where scriptc is installed. + const fileKeys = new Map(); + const fileKey = (file: string): string => { + let key = fileKeys.get(file); + if (key === undefined) fileKeys.set(file, (key = compilerOwnedFileName(file) ?? file)); + return key; + }; + const earlier = (left: SrcLoc, right: SrcLoc): boolean => { + if (left.file === right.file) return left.start < right.start; + return fileKey(left.file) < fileKey(right.file); + }; // The earliest use of each shared helper outside the shared helpers, and // which shared helpers each shared helper uses. const uses = new Map(); diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index f0f19a408..daa5af366 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -126,6 +126,7 @@ import { moduleRuntimeFeatures } from "./ir/runtime-features.js"; import { serializeModuleParts } from "./ir/serialize.js"; import { readTextParts, writeTextParts } from "./text-parts.js"; import { validateModule } from "./ir/validate.js"; +import { compilerOwnedFileRoots } from "./frontend/dts-paths.js"; import { loadProgram } from "./frontend/program-node.js"; import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js"; import { @@ -785,7 +786,7 @@ async function prepareExecutableInput( if (outputKind === "ir") { await mkdir(dirname(opts.outPath), { recursive: true }); - await writeTextParts(opts.outPath, serializeModuleParts(mod)); + await writeTextParts(opts.outPath, serializeModuleParts(mod, compilerOwnedFileRoots())); return { ok: true, artifact: { kind: "ir", path: opts.outPath } }; } @@ -885,7 +886,7 @@ async function prepareExecutableInput( if (opts.emitIr) { irPath = defaultSourcePaths.ir; irSourcePath = privateSiblingPath(irPath, "ir"); - await writeTextParts(irSourcePath, serializeModuleParts(mod)); + await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots())); } const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release"); @@ -1986,7 +1987,7 @@ async function emitSemanticLibraryHit( let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); - await writeTextParts(irPath, serializeModuleParts(mod)); + await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots())); } await compileLibraryNative(profile, llvmPath, archivePath, opts.sanitize ?? false, hit.native); timing("native-archive"); @@ -2272,7 +2273,7 @@ async function compileLibraryTracked( let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); - await writeTextParts(irPath, serializeModuleParts(mod)); + await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots())); } const nativeFeatures = libraryNativeFeatures(mod, profile.emission); diff --git a/packages/compiler/src/ir/serialize.test.ts b/packages/compiler/src/ir/serialize.test.ts index 8345ab10a..c24e1f198 100644 --- a/packages/compiler/src/ir/serialize.test.ts +++ b/packages/compiler/src/ir/serialize.test.ts @@ -198,3 +198,24 @@ test("byte decoding keeps the whole-document errors and version refusal", () => const sentinel = text.replace('"nan"', '"future"'); expect(() => deserializeModuleBytes(Buffer.from(sentinel))).toThrow("Invalid IR number sentinel"); }); + +test("file roots respell the source locations under them and nothing else", () => { + const mod = numbers([1, 2]); + const shim = { file: '/opt/scriptc "a"/shims/pkg/index.ts', start: 3, end: 4 }; + const other = { file: "/opt/elsewhere/shims/pkg/index.ts", start: 5, end: 6 }; + const body = mod.functions[0]!.body as IrStmt[]; + body.push( + { kind: "exprStmt", expr: { kind: "numLit", value: 3, type: F64, loc: shim }, loc: shim }, + { kind: "exprStmt", expr: { kind: "numLit", value: 4, type: F64, loc: other }, loc: other }, + ); + const roots = [{ from: '/opt/scriptc "a"/shims/', to: "scriptc:shims/" }]; + const relocated = deserializeModule(serializeModuleParts(mod, roots).join("")); + const files = (relocated.functions[0]!.body as IrStmt[]).map((stmt) => stmt.loc.file); + expect(files).toEqual([ + "numbers.ts", + "numbers.ts", + "scriptc:shims/pkg/index.ts", + "/opt/elsewhere/shims/pkg/index.ts", + ]); + expect(serializeModuleParts(mod).join("")).toBe(serializeModule(mod, true)); +}); diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index ca5eb955e..314f258d6 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -46,11 +46,41 @@ function encodeFunction(fn: IrFunction): string | undefined { return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber); } +/** A directory whose source location files an IR artifact names by a + * prefix of its own: a location file `from + rest` serializes as + * `to + rest`. */ +export interface SerializedFileRoot { + from: string; + to: string; +} + +/** Respells the source location files under `roots` in serialized parts. + * A location's file is the first member of its JSON object, and the + * escaped spelling of a path prefix is the prefix of the escaped path. */ +function relocateFiles(parts: string[], roots: readonly SerializedFileRoot[]): string[] { + const needles = roots.map(({ from, to }) => ({ + from: `"file":${JSON.stringify(from).slice(0, -1)}`, + to: `"file":${JSON.stringify(to).slice(0, -1)}`, + })); + return parts.map((part) => + needles.reduce( + (text, { from, to }) => (text.includes(from) ? text.replaceAll(from, to) : text), + part, + ), + ); +} + /** The compact encoding as ordered parts: the module header up to the * function array, one part per function (with separate comma parts), and the * header's remainder. Concatenating the parts yields exactly - * `serializeModule(mod, true)`; writers stream them to disk in batches. */ -export function serializeModuleParts(mod: IrModule): string[] { + * `serializeModule(mod, true)`; writers stream them to disk in batches. + * `fileRoots` respells the source locations in those directories, so + * artifacts do not depend on where the compiler that wrote them is + * installed. */ +export function serializeModuleParts( + mod: IrModule, + fileRoots: readonly SerializedFileRoot[] = [], +): string[] { // A replacer reads through live native views. Serializing the complete // function array through one view refreshes every function capsule for // each element, making large compiler artifacts quadratic. The sentinel @@ -65,7 +95,7 @@ export function serializeModuleParts(mod: IrModule): string[] { parts.push(encodeFunction(fn) ?? "null"); } parts.push("]" + header.slice(offset + slot.length)); - return parts; + return fileRoots.length === 0 ? parts : relocateFiles(parts, fileRoots); } /** Compiler artifacts use compact JSON; API consumers can retain the diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index a2c627edf..da90dc2ce 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -26,6 +26,7 @@ import { type FrontendInputExclusions, trackedReadFile, } from "../frontend/input-tracker.js"; +import { compilerOwnedFileRoots } from "../frontend/dts-paths.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; import { processFork } from "../frontend/process-fork.js"; import { prepareLibrary } from "../library/prepare.js"; @@ -312,7 +313,7 @@ export function prepareNativeExecutable( if (irInput !== "") timing("ir-input"); const ir = outputKind === "ir" || options.emitIr - ? batchTextParts(serializeModuleParts(prepared.mod)) + ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots())) : null; timing("ir-serialize"); const llvm = @@ -445,7 +446,9 @@ export function prepareNativeLibrary( const input: NativeExecutableInput = { ok: true, llvm, - ir: options.emitIr ? batchTextParts(serializeModuleParts(prepared.mod)) : null, + ir: options.emitIr + ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots())) + : null, sidecarJson: prepared.sidecarJson, features: executableLinkFeatures(prepared.mod), sources: [...prepared.sourceTexts], From 107d773382a6f83189b2ff959e5de89de6b3537e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:14:00 -0500 Subject: [PATCH 089/169] Pass zlib's flush sentinels to _transform as Buffers flush() on a zlib stream writes a zero-length chunk that _transform recognizes by identity and compresses with that flush kind. Since writes of a typed array that is not a Buffer reach _write as a Buffer over the same bytes (as Node's Stream._uint8ArrayToBuffer does), the sentinel, created as a plain Uint8Array, reached _transform as a new Buffer. _transform then used the default NO_FLUSH, so flush() emitted nothing: corpus 5374-zlib-stream-flush-params decoded an empty prefix where Node decodes "hello ". The sentinels are Buffers, as Node's kFlushBuffers are (Buffer.alloc(0)), so write() passes them through unchanged. The npm fixture stream-shims, whose test expected a deferred SC1090, now runs to a later deferred refusal, SC2020 (write completion callbacks taking the error argument); its expectation names that code. --- packages/runtime/src/scr_zlib_stream.c | 7 ++++++- tests/harness/npm.test.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 54ffacf02..40daa6c76 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -549,7 +549,12 @@ void scr_zlib_stream_flush(ScrStream *s, ScrDyn *kind, ScrClosure *cb) { return; } int ki = (int)k; - if (zs->flush_bufs[ki] == NULL) zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0); + if (zs->flush_bufs[ki] == NULL) { + /* a Buffer, as Node's kFlushBuffers are: write() passes a Buffer + * through unchanged, so _transform recognizes the sentinel */ + zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0); + zs->flush_bufs[ki]->is_buffer = true; + } scr_stream_write(s, zs->flush_bufs[ki], cb); } diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 795e42846..13ad3f438 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -102,7 +102,7 @@ const KNOWN: Record = { "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, "self-name": { build: "SC2013" }, - "stream-shims": { run: "SC1090" }, + "stream-shims": { run: "SC2020" }, "util-shims": { run: "SC2020" }, "web-streams": { run: "SC1090" }, "workspace-copied": { build: "SC2013" }, From a9b1d15eeac58cb104cc1cdbb75c89b285248a58 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:57:55 -0700 Subject: [PATCH 090/169] Map unresolved JSDoc results of generic JavaScript calls as checked-dynamic A published bundle's JSDoc can name a type that only its .d.ts declares: hono's bearerAuth carries `@template E` and `@returns {MiddlewareHandler}` without importing MiddlewareHandler. The checker types that result as its error type, and a call of such a generic JavaScript function was refused with SC2001 ("values of type 'MiddlewareHandler' cannot be compiled"). The unresolved annotation is documentation, not a contract, so the call's result is now checked-dynamic, as an `any` result of a TypeScript generic already is. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/generic-functions.ts | 9 +++- .../main.ts.json | 6 +++ .../jsdoc-unresolved-generic-return/main.ts | 19 ++++++++ .../npm/node_modules/jsdocmw/index.d.ts | 18 ++++++++ .../npm/node_modules/jsdocmw/index.js | 43 +++++++++++++++++++ .../npm/node_modules/jsdocmw/package.json | 8 ++++ 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json create mode 100644 tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts create mode 100644 tests/fixtures/npm/node_modules/jsdocmw/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/jsdocmw/index.js create mode 100644 tests/fixtures/npm/node_modules/jsdocmw/package.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index e27ab7f5f..222cb89f9 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -342,10 +342,15 @@ export function genericCallInstance( const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); if (local) return local; // A result instantiated at `any` (a type argument inferred from an - // untyped value, such as an untyped forwarding call) is checked-dynamic. + // untyped value, such as an untyped forwarding call) is checked-dynamic, + // and so is a JavaScript result whose JSDoc names a type the file never + // declares or imports (`@returns {MiddlewareHandler}` in a published + // bundle): the checker's error type is documentation it could not + // resolve, not a contract. const mappedReturn = lowerer.mapTypeOf(retTs) ?? - ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) + ((retTs.flags & ts.TypeFlags.Any) !== 0 && + (!isJsSourceFile(ts.getSourceFileOfNode(info.decl)) || retTs.isErrorType()) ? DYN : null); if (!mappedReturn) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json new file mode 100644 index 000000000..5395e20be --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts b/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts new file mode 100644 index 000000000..ce6ef5a94 --- /dev/null +++ b/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts @@ -0,0 +1,19 @@ +// A published bundle's generic factory whose JSDoc names its result by a +// type that only the package's .d.ts declares (hono's `bearerAuth`: +// `@template E` + `@returns {MiddlewareHandler}`), passed straight into +// a generic registration method. The unresolved JSDoc result is not a +// contract, so the call's result is checked-dynamic. +import { guard, Router } from "jsdocmw"; + +const router = new Router(); +router.on("/admin", guard({ token: "s3cret" }), async (ctx, next) => { + ctx.log.push(`handler ${ctx.path}`); + await next(); +}); +const open = guard({ token: "" }); +router.on("/open", open, async (ctx) => { + ctx.log.push(`open handler ${ctx.path}`); +}); +console.log(await router.dispatch("/admin", "s3cret")); +console.log(await router.dispatch("/admin", "nope")); +console.log(await router.dispatch("/open", "")); diff --git a/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts b/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts new file mode 100644 index 000000000..1c93f3348 --- /dev/null +++ b/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts @@ -0,0 +1,18 @@ +export interface Env { + Variables?: Record; +} +export interface Ctx { + path: string; + token: string; + log: string[]; + env?: E; +} +export type Middleware = (ctx: Ctx, next: () => Promise) => Promise; +export interface GuardOptions { + token: string; +} +export declare const guard: (options: GuardOptions) => Middleware; +export declare class Router { + on(path: string, ...handlers: Middleware[]): this; + dispatch(path: string, token: string): Promise; +} diff --git a/tests/fixtures/npm/node_modules/jsdocmw/index.js b/tests/fixtures/npm/node_modules/jsdocmw/index.js new file mode 100644 index 000000000..f1e38383b --- /dev/null +++ b/tests/fixtures/npm/node_modules/jsdocmw/index.js @@ -0,0 +1,43 @@ +/** + * Guard middleware. + * + * @template E - The environment type. + * @param {GuardOptions} options - The options. + * @returns {Middleware} The middleware. + */ +export const guard = (options) => { + return async function guard(ctx, next) { + if (ctx.token !== options.token) { + ctx.log.push(`denied ${ctx.path}`); + return; + } + ctx.log.push(`allowed ${ctx.path}`); + await next(); + }; +}; + +export class Router { + constructor() { + this.routes = []; + } + + on(path, ...handlers) { + this.routes.push({ path, handlers }); + return this; + } + + async dispatch(path, token) { + const log = []; + for (const route of this.routes) { + if (route.path !== path) continue; + const ctx = { path, token, log }; + let index = 0; + const next = async () => { + const handler = route.handlers[index++]; + if (handler) await handler(ctx, next); + }; + await next(); + } + return log; + } +} diff --git a/tests/fixtures/npm/node_modules/jsdocmw/package.json b/tests/fixtures/npm/node_modules/jsdocmw/package.json new file mode 100644 index 000000000..a3283642f --- /dev/null +++ b/tests/fixtures/npm/node_modules/jsdocmw/package.json @@ -0,0 +1,8 @@ +{ + "name": "jsdocmw", + "version": "1.0.0", + "description": "a bundled middleware factory whose JSDoc @template result names a type only its .d.ts declares (hono's bearerAuth shape)", + "type": "module", + "main": "index.js", + "types": "index.d.ts" +} From 74f2c9754b4540fe67703c1cda5d4b33adc033e4 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:03:18 -0700 Subject: [PATCH 091/169] Dispatch String-only method names on untyped receivers by runtime kind A call of a String.prototype-only method name (trim, toLowerCase, startsWith, ...) on a checker-untyped receiver was lowered as a string call that throws unless the receiver is a string. A checked-dynamic object can carry its own member of that name: zod's schemas assign `trim` in their constructors, so `z.string().trim().min(1)` typed the `trim()` result as a string and refused `.min` with SC1090. Untyped receivers now take the runtime-kind dispatch that object-typed receivers missing the member already use: a string runs the string lowering, any other value calls its own member as Node does. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-calls.ts | 23 ++++++----- ...d-receiver-own-string-method-names.ts.json | 6 +++ ...ntyped-receiver-own-string-method-names.ts | 38 +++++++++++++++++++ ...d-receiver-own-string-method-names.ts.json | 1 + 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json create mode 100644 tests/corpus/7441-untyped-receiver-own-string-method-names.ts create mode 100644 tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c1bc044ae..784efeb13 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3797,18 +3797,21 @@ function lowerDynReceiverMethodCall( } if (access.name.text === "flatMap") return lowerDynArrayFlatMapCall(lowerer, call, access, recv); // String methods claim only names NO other dyn-representable kind's - // prototype declares (Array carries includes/indexOf/slice too): for - // these, "the receiver is a string, or the call throws V8's TypeError" - // IS Node's semantics for every possible dyn value. Shared names would - // need a receiver-kind dispatch — they keep the fence. + // prototype declares (Array carries includes/indexOf/slice too), so no + // other kind inherits them. Shared names would need a receiver-kind + // dispatch — they keep the fence. if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { - // An object-typed receiver the member is missing from (a JavaScript - // module object whose members are assigned in a function body, - // `common.repeat(" ", n)`) holds a string or an object with that - // member: dispatch on the receiver's runtime kind. + // A checked-dynamic object can still carry its OWN member of that + // name: zod's schemas assign `trim`, `toLowerCase` and `startsWith` + // in their constructors (`z.string().trim().min(1)`), and a JavaScript + // module object can assign `repeat` in a function body + // (`common.repeat(" ", n)`, an object-typed receiver the member is + // missing from). An untyped receiver, or a member the checker cannot + // type, dispatches on the receiver's runtime kind: a string runs the + // string lowering, anything else calls its member as Node does. if ( - !checkerUntyped && - (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !call.questionDotToken && !access.questionDotToken && !call.arguments.nodes.some(ts.isSpreadElement) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json new file mode 100644 index 000000000..7477193ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7441-untyped-receiver-own-string-method-names.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7441-untyped-receiver-own-string-method-names.ts b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts new file mode 100644 index 000000000..632ceb16f --- /dev/null +++ b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts @@ -0,0 +1,38 @@ +// An untyped receiver calling a String.prototype method name holds a +// string or a checked-dynamic object with its own method of that name +// (zod's schemas assign `trim`, `toLowerCase` and `startsWith` in their +// constructors, so `z.string().trim().min(1)` chains schema calls). +// Each call dispatches on the receiver's runtime kind. +interface Field { + value: string; + checks: string[]; +} + +function field(value: string, checks: string[]): any { + const self: Field & Record = { value, checks }; + self["trim"] = () => field(value.trim(), [...checks, "trim"]); + self["toLowerCase"] = () => field(value.toLowerCase(), [...checks, "lower"]); + self["startsWith"] = (prefix: string) => checks.length > 0 && value.startsWith(prefix); + self["min"] = (n: number) => field(value, [...checks, `min(${n})`]); + return self; +} + +const chained = field(" Hello World ", []).trim().toLowerCase().min(1); +console.log(JSON.stringify(chained.value), chained.checks.join(",")); +console.log(field("abc", ["x"]).startsWith("ab"), field("abc", []).startsWith("ab")); + +const text: any = " Mixed Case "; +console.log(JSON.stringify(text.trim()), text.toLowerCase(), text.startsWith(" M")); + +const parsed: any = JSON.parse('{"name":" padded "}'); +console.log(JSON.stringify(parsed.name.trim())); + +for (const value of [field(" a ", []), " b ", 42, null]) { + const v: any = value; + try { + const out = v.trim(); + console.log(typeof out === "string" ? JSON.stringify(out) : `field ${JSON.stringify(out.value)}`); + } catch (error) { + console.log(error instanceof TypeError, (error as Error).message); + } +} diff --git a/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json new file mode 100644 index 000000000..615c617ae --- /dev/null +++ b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json @@ -0,0 +1 @@ +{ "plain": 5800 } From 6115135d96affbf01e4960f270e9e7eac2e22ed5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:18:48 -0500 Subject: [PATCH 092/169] Dispatch new.target over generic subclasses and own static names A constructor's new.target dispatch skipped generic class families, so a generic subclass instantiated only in a body lowered later answered with the base's name, and `new.target === Base` held for its instances. A family now has its own arm, tested by its instanceof interval, which holds every instantiation: `new.target.name` reads the family's name and comparisons with a program class are false. Under an instantiation, the generic subclasses of its family are candidates too. `new.target` as a value refuses generic subclasses, which have no common class value. `new.target.name` reads a class's own static `name` field, which replaces its name (a subclass has its own name); a static method `name` or a non-string field is refused. --- .../src/frontend/lowering/new-target.ts | 68 ++++++++++++++---- ...364-new-target-late-instantiations.ts.json | 6 ++ .../new-target-value-generic-subclass.ts.json | 6 ++ .../7364-new-target-late-instantiations.ts | 71 +++++++++++++++++++ .../new-target-value-generic-subclass.ts | 5 ++ .../new-target-value-generic-subclass.ts.txt | 6 ++ ...364-new-target-late-instantiations.ts.json | 1 + 7 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json create mode 100644 tests/corpus/7364-new-target-late-instantiations.ts create mode 100644 tests/diagnostics/new-target-value-generic-subclass.ts create mode 100644 tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt create mode 100644 tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json diff --git a/packages/compiler/src/frontend/lowering/new-target.ts b/packages/compiler/src/frontend/lowering/new-target.ts index 8b5f5858a..4cf271469 100644 --- a/packages/compiler/src/frontend/lowering/new-target.ts +++ b/packages/compiler/src/frontend/lowering/new-target.ts @@ -43,7 +43,9 @@ function bindingOf(node: ts.Node): Binding { interface Dispatch { info: ClassInfo; - /** The constructor's class and its descendants, deepest first. */ + /** The constructor's descendants, deepest first, then its class. A + * generic family stands for its instantiations, also those that later + * bodies instantiate: its interval holds them all. */ classes: ClassInfo[]; self: IrExpr | null; } @@ -69,11 +71,23 @@ function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { ); return null; } - const classes = [info]; + // An instantiation's subclasses can also be instantiations of generic + // subclasses of its family (`class G extends Box` under Box). + const family = info.genericInstance?.family; + const descendants: ClassInfo[] = []; for (const other of lowerer.classes.values()) { - if (other === info || other.generic) continue; - if (lowerer.isSubclassOf(other.def.name, info.def.name)) classes.push(other); + if (other === info) continue; + if ( + lowerer.isSubclassOf(other.def.name, info.def.name) || + (family !== undefined && + other.generic !== undefined && + other !== family && + lowerer.isSubclassOf(other.def.name, family.def.name)) + ) + descendants.push(other); } + descendants.sort((a, b) => depth(b) - depth(a)); + const classes = [...descendants, info]; for (const c of classes) { if (c.localClass) { lowerer.unsupported( @@ -84,7 +98,6 @@ function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { return null; } } - classes.sort((a, b) => depth(b) - depth(a)); let self: IrExpr | null = null; if (classes.length > 1) { const thisLocal = lowerer.resolveThis(expr); @@ -140,6 +153,13 @@ export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr const dispatch = dispatchOf(lowerer, expr)!; const type: IrType = { kind: "classval", className: dispatch.info.def.name }; for (const c of dispatch.classes) { + if (c.generic) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' as a value where '${c.def.jsName ?? c.def.name}' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead)`, + ); + } if (c !== dispatch.info && !lowerer.ctorAbiEquals(c, dispatch.info)) { lowerer.unsupported( "SC1090", @@ -166,13 +186,37 @@ export function lowerNewTargetName( const dispatch = dispatchOf(lowerer, target); if (!dispatch) return null; const loc = locOf(access); - return select(dispatch, STRING, loc, (c) => ({ - kind: "libCall", - fn: "class.name", - args: [classValueRef(lowerer, c, target)], - type: STRING, - loc, - })); + return select(dispatch, STRING, loc, (c) => { + // An own static `name` field replaces the class's name; a subclass has + // its own name. An instantiation's statics are its family's. + const owner = c.genericInstance?.family ?? c; + const field = owner.staticFields.find((f) => f.name === "name"); + if (field !== undefined) { + if (field.type.kind !== "string") + lowerer.unsupported( + "SC1090", + access, + `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static 'name' that is not a string`, + ); + return { kind: "varRef", localId: field.globalId, type: field.type, loc }; + } + if (owner.staticMethods?.has("name") || owner.genericStatics?.has("name")) + lowerer.unsupported( + "SC1090", + access, + `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static method 'name'`, + ); + // A generic family's instantiations share its name. + if (c.generic) + return { kind: "strLit", value: c.def.jsName ?? c.generic.baseName, type: STRING, loc }; + return { + kind: "libCall", + fn: "class.name", + args: [classValueRef(lowerer, c, target)], + type: STRING, + loc, + }; + }); } /** `new.target === C` (and `!==`, `==`, `!=`, either operand order) with a diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json new file mode 100644 index 000000000..20d761c6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7364-new-target-late-instantiations.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json new file mode 100644 index 000000000..1abcce3ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-value-generic-subclass.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7364-new-target-late-instantiations.ts b/tests/corpus/7364-new-target-late-instantiations.ts new file mode 100644 index 000000000..3fa7ff94a --- /dev/null +++ b/tests/corpus/7364-new-target-late-instantiations.ts @@ -0,0 +1,71 @@ +// new.target in a constructor whose subclasses are generic classes +// instantiated only in bodies lowered later, and classes whose own static +// `name` field replaces their name. +class Plain { + readonly label: string; + readonly plain: boolean; + constructor() { + this.label = new.target.name; + this.plain = new.target === Plain; + } +} +class Sub extends Plain { + readonly v: T; + constructor(v: T) { + super(); + this.v = v; + } +} + +class Box { + readonly label: string; + readonly exact: boolean; + readonly value: T; + constructor(value: T) { + this.value = value; + this.label = new.target.name; + this.exact = new.target === Leaf; + } +} +class Tagged extends Box {} +class Leaf extends Tagged {} + +class Named { + static name = "custom"; + readonly label: string; + constructor() { + this.label = new.target.name; + } +} +class Child extends Named {} +class Other extends Named { + static name = "other"; +} +class Family { + static name = "family"; + readonly label: string; + readonly v: T; + constructor(v: T) { + this.label = new.target.name; + this.v = v; + } +} + +function later(): void { + const n = new Sub(1); + const p = new Plain(); + console.log(n.label, new Sub("s").label, p.label, n.plain, p.plain); + const t = new Tagged(1); + const s = new Tagged("a"); + const b = new Box(2); + const l = new Leaf(3); + console.log(t.label, s.label, b.label, l.label, t.exact, l.exact, b.exact); + console.log(new Family(true).label); +} + +// The bases construct here first; their subclasses only in later(). +console.log(new Plain().label, new Box(false).label); +console.log(new Named().label, new Child().label, new Other().label); +Named.name = "changed"; +console.log(new Named().label, new Child().label, new Other().label); +later(); diff --git a/tests/diagnostics/new-target-value-generic-subclass.ts b/tests/diagnostics/new-target-value-generic-subclass.ts new file mode 100644 index 000000000..2107a39a5 --- /dev/null +++ b/tests/diagnostics/new-target-value-generic-subclass.ts @@ -0,0 +1,5 @@ +// new.target as a class value where a subclass is generic: its +// instantiations have no common class value. +class A { readonly t: typeof A; constructor() { this.t = new.target; } } +class B extends A { readonly v: T | undefined = undefined; } +console.log(new B().t.name); diff --git a/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt new file mode 100644 index 000000000..0e5cf2e52 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-value-generic-subclass.ts:3:58 - error SC1090: 'new.target' as a value where 'B' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet + + 2 | // instantiations have no common class value. + 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } } + | ^~~~~~~~~~ + 4 | class B extends A { readonly v: T | undefined = undefined; } \ No newline at end of file diff --git a/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json new file mode 100644 index 000000000..725cf769c --- /dev/null +++ b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 17500 } From 5edf9df5ef666cde604fdc3b19c35c2cb8ac0049 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:19:05 -0500 Subject: [PATCH 093/169] Read only IR nodes as IR nodes in the code-unit locals pass --- packages/compiler/src/ir/char-unit-locals.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index 4008f4e1e..beeeffaf3 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -161,6 +161,10 @@ function unitLocal(local: IrLocal): IrLocal { * declaration from a one-argument charAt, a one-unit test, or a switch * over one-unit cases. */ function understoodMention(node: Record): string | null { + // count() visits every object in the body (types, locations, cases), so + // only the node kinds handled here are read as IR nodes + const kind = node.kind; + if (kind !== "varDecl" && kind !== "strEq" && kind !== "strCmp" && kind !== "switch") return null; const n = node as unknown as IrExpr | IrStmt; if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null; if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null; From 8b5e8617702fd2438dfb049d4cf8d66961c10aa4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:21:09 -0500 Subject: [PATCH 094/169] Give each class one prototype in identity comparisons of unknown values `Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`, for two instances held as unknown, compared fresh objects when their class has no prototype data (no methods, or a subclass of Error), so it was false for instances of one class. Each such class now has one stand-in object, created on first use. A subclass instance boxed at its base type answered with the base's prototype, so instances of two sibling subclasses compared true. The answer for a value boxed at a class now tests the class's subclasses, most derived first, and answers with the prototype of the one the instance belongs to. --- .../lowering/class-dynamic-dispatch.ts | 120 +++++++++++++++--- .../7541-prototype-identity-stand-ins.ts.json | 6 + ...42-prototype-identity-without-data.ts.json | 6 + .../7541-prototype-identity-stand-ins.ts | 22 ++++ .../7542-prototype-identity-without-data.ts | 19 +++ .../7541-prototype-identity-stand-ins.ts.json | 1 + ...42-prototype-identity-without-data.ts.json | 1 + 7 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json create mode 100644 tests/corpus/7541-prototype-identity-stand-ins.ts create mode 100644 tests/corpus/7542-prototype-identity-without-data.ts create mode 100644 tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json create mode 100644 tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index b18615ef6..8dd5fd2c1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -909,6 +909,9 @@ export class ClassDynamicDispatch { private descriptorHelper: IrFunction | null = null; private prototypeHelper: IrFunction | null = null; private readonly prototypeClasses = new Set(); + /** The stand-in prototype object of each class without prototype data + * (prototypeStandIn), by class name. */ + private readonly prototypeStandIns = new Set(); /** The prototype reads of identity walks, by libCall, and the classes * each one answers for boxed instances. */ private readonly prototypeIdentityHelpers = new Map< @@ -1181,32 +1184,76 @@ export class ClassDynamicDispatch { const value = varRef("value", DYN, loc); // A class with no prototype data in its chain (one extending // Error, or with no methods) has no prototype a program can name: - // a fresh object stands in, so an identity comparison or an - // instanceof walk (through Object.prototype to null) answers false - // instead of reaching the reflection refusal. - const standIn: IrExpr = { kind: "dynObjLit", fields: [], type: DYN, loc }; - const prototype = hasClassPrototypeData(info) - ? classPrototypeData(lowerer, info, loc, { - kind: "dynCheck", - value, - type, - loc, - }) - : standIn; - if (prototype) - helper.fn.body.unshift({ + // an object of the class's own stands in, so instances of one class + // share it (`Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`), + // other prototypes differ from it, and an instanceof walk (through + // Object.prototype to null) answers false instead of reaching the + // reflection refusal. + const answer = (name: string): IrStmt[] | null => { + const of = lowerer.classes.get(name)!; + const prototype = hasClassPrototypeData(of) + ? classPrototypeData(lowerer, of, loc, { + kind: "dynCheck", + value, + type: { kind: "object", className: name }, + loc, + }) + : null; + if (prototype) return [{ kind: "return", value: prototype, loc }]; + return hasClassPrototypeData(of) ? null : this.prototypeStandIn(lowerer, name, loc); + }; + const own = answer(className); + if (!own) { + changed = true; + continue; + } + // A capsule boxed at this class can hold a subclass instance: the + // most derived subclass the instance belongs to answers. + const depth = (name: string): number => { + let n = 0; + for (let at = lowerer.classes.get(name)?.base; at; at = at.base) n++; + return n; + }; + const subclasses = [...lowerer.classes.keys()] + .filter( + (name) => + name !== className && + lowerer.isSubclassOf(name, className) && + isDynTypedRefType({ kind: "object", className: name }), + ) + .sort((a, b) => depth(b) - depth(a)); + const then: IrStmt[] = []; + for (const name of subclasses) { + const sub = answer(name); + if (!sub) continue; + then.push({ kind: "if", cond: { - kind: "libCall", - fn: "dyn.typedRefIs", - args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + kind: "instanceOf", + value: { kind: "dynCheck", value, type, loc }, + className: name, type: BOOL, loc, }, - then: [{ kind: "return", value: prototype, loc }], + then: sub, else_: null, loc, }); + } + then.push(...own); + helper.fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then, + else_: null, + loc, + }); changed = true; } for (const plan of this.propertyReceivers(lowerer, false, false)) { @@ -3528,6 +3575,43 @@ export class ClassDynamicDispatch { /** The receiver's own dynamic properties for a read: undefined while it has * none, and never stored, so reads stay valid on published objects. */ + /** Statements that return `className`'s stand-in prototype: one empty + * object per class, created on first use and kept in a module global. */ + private prototypeStandIn(lowerer: Lowerer, className: string, loc: SrcLoc): IrStmt[] { + const globalId = `%g.%prototype.standIn.${className}`; + const readyId = `${globalId}.ready`; + if (!this.prototypeStandIns.has(className)) { + this.prototypeStandIns.add(className); + lowerer.globalsList.push( + { id: globalId, name: `${className}.prototype`, type: DYN, mutable: true }, + { id: readyId, name: `${className}.prototype.ready`, type: BOOL, mutable: true }, + ); + } + return [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, + then: [ + { + kind: "assign", + localId: globalId, + value: { kind: "dynObjLit", fields: [], type: DYN, loc }, + loc, + }, + { + kind: "assign", + localId: readyId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: varRef(globalId, DYN, loc), loc }, + ]; + } + private readPropertyBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr { const helper = classPropertiesHelper(lowerer, loc, true); this.generated.add(helper); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..c5179b475 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7541-prototype-identity-stand-ins.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..09f1a6369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7542-prototype-identity-without-data.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7541-prototype-identity-stand-ins.ts b/tests/corpus/7541-prototype-identity-stand-ins.ts new file mode 100644 index 000000000..81b1e8c71 --- /dev/null +++ b/tests/corpus/7541-prototype-identity-stand-ins.ts @@ -0,0 +1,22 @@ +// Instances held as unknown, of a class whose chain has no prototype data +// (a subclass without methods, or one extending Error), still share one +// prototype: comparing the prototypes of two instances of the same class is +// true, and of instances of different classes is false. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const first: unknown = bases[0]; +const second: unknown = bases[1]; +const other: unknown = bases[2]; +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(second)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(other)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(e1)); +console.log(first instanceof Sub, other instanceof Sub, e1 instanceof Oops, e1 instanceof Error); diff --git a/tests/corpus/7542-prototype-identity-without-data.ts b/tests/corpus/7542-prototype-identity-without-data.ts new file mode 100644 index 000000000..9ac1209f8 --- /dev/null +++ b/tests/corpus/7542-prototype-identity-without-data.ts @@ -0,0 +1,19 @@ +// Instances held as unknown, of classes whose chains have no prototype data +// a program can name (no methods, or a subclass of Error): each class's +// instances share one prototype, and different classes' differ, as in Node. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other(), new Base()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const [a, b, c, d] = bases.map((value): unknown => value); +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(d)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(e1)); diff --git a/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..046e0461c --- /dev/null +++ b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 6500 } diff --git a/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..d2c83f967 --- /dev/null +++ b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 } From 33cf76d36e4fa971aff022c75df66eee16bef38a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:23:57 -0500 Subject: [PATCH 095/169] Keep object and array literals that hold an evolving empty array on that array An object or array literal that held an evolving `const xs = []` between writes of different element types (`const box = { items: xs }` or `const rows = [xs]` after `xs.push(1)`, before `xs.push("two")`) took the element type the checker gives the array at that point, so it held a copy. `box.items.length` and `box.items === xs` printed `1 false`, where Node prints `2 true`. Such a literal now builds as a checked-dynamic literal that keeps the array, and an unannotated binding of it takes the checked-dynamic type. This covers shorthand properties (`{ xs }`), nested literals, spreads of a literal holder, aliases of a holder and names destructured from one. Element reads through an alias or holder (`rows[0]`, `alias[1]`) read the checked-dynamic value instead of checking it against the checker's type, which lists only the elements written before the alias or holder was taken. A shorthand property now counts as a read of the array when its reads are compared, so `{ xs }` before a write of another element type makes the array checked-dynamic too. Fixes #1641. --- .../lowering/expressions/element-access.ts | 15 ++- .../lowering/expressions/object-literals.ts | 6 +- .../src/frontend/lowering/lower-stmts.ts | 126 +++++++++++++++--- .../compiler/src/frontend/lowering/lowerer.ts | 4 +- tests/corpus/7006-evolving-array-aliases.ts | 48 +++++++ 5 files changed, 175 insertions(+), 24 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 045bebdf4..a9246ceec 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -65,6 +65,7 @@ import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js"; import { isClassCallback } from "../class-callbacks.js"; import { isKeyedFieldAccess } from "./keyed-field-targets.js"; import { lowerFieldCompoundValue } from "./updates.js"; +import { readsHeldEvolvingArray } from "../lower-stmts.js"; /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS * string-key element access (`a["length"]`) and string indexing (`s[0]` @@ -751,12 +752,16 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre : lowerer.maybeNarrow(arr, expr.expression); } + // An alias or container of an evolving array whose reads disagree + // (`rows[0]` after `const rows = [xs]`) holds elements the checker's type + // does not list, so its element read stays checked-dynamic. + const heldEvolving = arr.type.kind === "dyn" && readsHeldEvolvingArray(lowerer, expr.expression); // The checker sees an array but the VALUE lowered checked-dynamic (a // jsdoc-typed default export from a .js module — signature 14): the // read rides the dynCheck boundary when the array shape is one the checked-dynamic tree // can validate, and fences by name when it is not — an arrayGet over a // dyn receiver is never emitted (the validator ICE). - if (arr.type.kind === "dyn") { + if (arr.type.kind === "dyn" && !heldEvolving) { if ( canDynCheckTo( receiverIr, @@ -782,6 +787,14 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre locOf(expr.argumentExpression), expr.argumentExpression, ); + if (heldEvolving) + return { + kind: "dynKeyGet", + value: arr, + key: lowerer.coerceToExpected(index, DYN), + type: DYN, + loc: locOf(expr), + }; if ( arr.type.kind === "array" && (index.type.kind === "dyn" || diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 4d04be30a..429abf793 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -37,6 +37,7 @@ import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; import { builtinGlobalValue } from "./property-access.js"; +import { holdsEvolvingArray } from "../lower-stmts.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -397,7 +398,10 @@ export function lowerDynObjectLiteral( ? lowerer.lowerLambda(prop, true) : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) ? shorthandBuiltinValue(lowerer, prop)! - : !boxValue && ts.isObjectLiteralExpression(valueExpr) + : !boxValue && + (ts.isObjectLiteralExpression(valueExpr) || + (ts.isArrayLiteralExpression(valueExpr) && + holdsEvolvingArray(lowerer, valueExpr))) ? lowerer.lowerExprExpecting(valueExpr, DYN) : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 62c2e3a0f..bb045ac91 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -4920,12 +4920,16 @@ export function bindPatternTarget( } // Preserve checked-value pattern bindings when the inferred type // cannot represent them without a refusal or an alias-breaking copy. + // A name destructured from a container of an evolving array + // (evolvingArrayHold) may hold the array, which the checker types by + // the elements written before it. const dynStays = value.type.kind === "dyn" && - (() => { + ((() => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(name)); return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped); - })(); + })() || + readsHeldEvolvingArray(lowerer, name)); let type = dynStays ? DYN : (lowerer.runtimeOptionalBindingType(name, lowerer.irTypeOf(name)) ?? @@ -5054,13 +5058,16 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy // type once the reads settle it. const written: ts.Expression[] = []; // The file's identifiers are indexed by name once (identifiersNamed), so - // each binding resolves only the uses spelled like it. + // each binding resolves only the uses spelled like it. A shorthand + // property (`{ xs }`) is a use too. for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) { if ( node === decl.name || node.pos < scope.pos || node.end > scope.end || - lowerer.checker.getSymbolAtLocation(node) !== symbol + (ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node + ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) + : lowerer.checker.getSymbolAtLocation(node)) !== symbol ) continue; let target: ts.Node = node; @@ -5118,28 +5125,105 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy return result; } -/** An unannotated alias of an evolving array binding whose reads disagree - * (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the - * checker types the alias by the elements written before it, but both names - * hold one array, so the alias takes the binding's checked-dynamic type and - * sees the later writes. Aliases of such an alias follow it. Null for other - * bindings. */ -function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { - let current = decl; - for (let depth = 0; depth < 8; depth++) { - let initializer = current.initializer; - while (initializer !== undefined && ts.isParenthesizedExpression(initializer)) - initializer = initializer.expression; - if (initializer === undefined || !ts.isIdentifier(initializer)) return null; - const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration; +/** How a TypeScript expression holds an evolving array binding whose reads + * disagree (evolvingArrayType answers checked-dynamic): "evolving" for the + * binding itself, "alias" for an unannotated binding initialized with it or + * with an alias of it (`const alias = xs`), and "container" for an object or + * array literal with it as a property, element or spread source at any + * depth (`{ items: xs }`, `[xs]`), an unannotated binding initialized with + * or destructured from such a literal, and a property or element read of a + * container (`box.items`). The checker types an alias or container by the + * elements written before it was taken, but it holds the same array. Null + * for other expressions. */ +function evolvingArrayHold( + lowerer: Lowerer, + expr: ts.Expression, + depth = 0, +): "evolving" | "alias" | "container" | null { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (depth >= 8) return null; + const binding = (symbol: ts.Symbol | undefined): "evolving" | "alias" | "container" | null => { + const source = symbol?.valueDeclaration; + // A name destructured from a container (`const { items } = box`) may + // be the array or another container. + if (source !== undefined && ts.isBindingElement(source) && ts.isIdentifier(source.name)) { + const host = hostVariableDeclarationOf(source.name); + return host !== null && + !host.type && + host.initializer !== undefined && + evolvingArrayHold(lowerer, host.initializer, depth + 1) === "container" + ? "container" + : null; + } if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null; const evolved = evolvingArrayType(lowerer, source); - if (evolved !== null) return evolved.kind === "dyn" ? evolved : null; - current = source; + if (evolved !== null) return evolved.kind === "dyn" ? "evolving" : null; + if (source.initializer === undefined) return null; + const held = evolvingArrayHold(lowerer, source.initializer, depth + 1); + return held === "evolving" ? "alias" : held; + }; + if (ts.isIdentifier(expr)) return binding(lowerer.checker.getSymbolAtLocation(expr)); + if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) + return evolvingArrayHold(lowerer, expr.expression, depth + 1) === "container" + ? "container" + : null; + // A spread of the array itself (`{ ...xs }`, `[...xs]`) copies its + // elements, as in Node; a spread of a container copies its references. + if (ts.isObjectLiteralExpression(expr)) { + for (const property of expr.properties.nodes) { + const held = ts.isPropertyAssignment(property) + ? evolvingArrayHold(lowerer, property.initializer, depth + 1) + : ts.isShorthandPropertyAssignment(property) + ? binding(lowerer.checker.getShorthandAssignmentValueSymbol(property)) + : ts.isSpreadAssignment(property) && + evolvingArrayHold(lowerer, property.expression, depth + 1) === "container" + ? "container" + : null; + if (held !== null) return "container"; + } + return null; + } + if (ts.isArrayLiteralExpression(expr)) { + for (const element of expr.elements.nodes) { + const held = ts.isSpreadElement(element) + ? evolvingArrayHold(lowerer, element.expression, depth + 1) === "container" + ? "container" + : null + : evolvingArrayHold(lowerer, element, depth + 1); + if (held !== null) return "container"; + } } return null; } +/** Whether a TypeScript expression is an alias or container of an evolving + * array binding whose reads disagree (evolvingArrayHold). Its value is + * checked-dynamic, and the checker's type for it lists only the elements + * written before it was taken, so element reads through it stay + * checked-dynamic instead of checking against that type. */ +export function readsHeldEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const held = evolvingArrayHold(lowerer, expr); + return held === "alias" || held === "container"; +} + +/** Whether a TypeScript object or array literal holds an evolving array + * binding whose reads disagree (evolvingArrayHold), so it builds as a + * checked-dynamic literal that keeps the array. */ +export function holdsEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + return evolvingArrayHold(lowerer, expr) === "container"; +} + +/** An unannotated binding initialized with an evolving array binding whose + * reads disagree, or with an alias or container of one (evolvingArrayHold): + * it takes the array's checked-dynamic type, so it sees the later writes. + * Null for other bindings. */ +function evolvingArrayHolderType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null { + if (decl.initializer === undefined) return null; + return evolvingArrayHold(lowerer, decl.initializer) === null ? null : DYN; +} + /** A single static type for an initializer-less JavaScript `let` whose * later plain writes all agree. An evolving JS local reads as `undefined` * until control flow proves a write, so a scalar slot is sound only when @@ -5834,7 +5918,7 @@ export function lowerVarDecl( !decl.type && !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { - const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl); + const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayHolderType(lowerer, decl); if (evolved !== null) { type = evolved; init = lowerer.lowerExprExpecting(decl.initializer, evolved); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..80db45f3a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -614,6 +614,7 @@ import { lowerForStatement, matchGroupsPatternSource, isGuardedRecordRead, + holdsEvolvingArray, } from "./lower-stmts.js"; import { type FieldTarget, @@ -10655,7 +10656,8 @@ export class Lowerer { builtinOption("UrlObject") || builtinOption("URLFormatOptions") || !this.mapTypeOf(this.typeOf(x)) || - this.excessJsContextualProperty(x, parts) + this.excessJsContextualProperty(x, parts) || + holdsEvolvingArray(this, x) ) { return lowerDynObjectLiteral(this, x); } diff --git a/tests/corpus/7006-evolving-array-aliases.ts b/tests/corpus/7006-evolving-array-aliases.ts index 215b3f3c5..fce1d0946 100644 --- a/tests/corpus/7006-evolving-array-aliases.ts +++ b/tests/corpus/7006-evolving-array-aliases.ts @@ -77,3 +77,51 @@ function spreadSame(): number { return xs.reduce((sum, v) => sum + v, 0); } console.log(spreadSame()); + +// An object or array literal that holds the array between the writes +// (`{ items: xs }`, `[xs]`, a shorthand property, a nested literal), a +// binding of such a literal, and a name destructured from one hold the +// same array, so they see the later writes. +function objectHolder(): string { + const xs = []; + xs.push(1); + const box = { items: xs, label: "box" }; + xs.push("two"); + box.items.push(3); + return `${box.items.length} ${box.items === xs} ${box.items[1]} ${box.label} ${xs.join(",")}`; +} +function arrayHolder(): string { + const xs = []; + xs.push(1); + const rows = [xs, [5]]; + xs.push("two"); + let total = 0; + for (const row of rows) total += row.length; + return `${rows[0].length} ${rows[0] === xs} ${rows[0][1]} ${total}`; +} +function shorthandHolder(): string { + const xs = []; + xs.push(1); + const box = { xs, count: 3 }; + xs.push("two"); + return `${box.xs.length} ${box.xs === xs} ${box.count}`; +} +function nestedHolder(): string { + const xs = []; + xs.push(1); + const box = { inner: [xs], pair: [{ items: xs }] }; + const outer = { box }; + xs.push("two"); + return `${box.inner[0].length} ${box.pair[0].items === xs} ${outer.box.inner[0][1]}`; +} +function holderAliases(): string { + const xs = []; + xs.push(1); + const box = { items: xs }; + const other = box; + const { items } = box; + xs.push("two"); + return `${other.items.length} ${items.length} ${items === xs} ${JSON.stringify(other)}`; +} +console.log(objectHolder(), "|", arrayHolder(), "|", shorthandHolder()); +console.log(nestedHolder(), "|", holderAliases()); From 9bf76af9745b368c83ac6051da0b08e143736c4b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:43:59 -0700 Subject: [PATCH 096/169] Build @hono/node-server 2.x into scriptc @hono/node-server replaces the global Request and Response with lightweight objects and reparents prototypes, tricks that only pay off under V8 and that native compilation cannot follow. scriptc now compiles the package from a built-in implementation (shims/hono-node-server/v2.ts) over node:http and the native Request/Response/Headers: serve, createAdaptorServer, getRequestListener and RequestError. Responses are written the way the package writes them, byte for byte on the wire: the general path (header record in Headers order, Set-Cookie gathered, default content type, content length for bodies that finish within the first reads, streamed bodies, error responses) and the package's cached-response path for a Response that is still exactly its constructor arguments. The runtime tracks that lightweight state on each constructed Response (header form, raw plain-record headers, body length; cleared by body and metadata reads, turned into Headers form by a headers read) and exposes it to the shim through scriptcResponseLight. The declaration entry of a shimmed package may now be a .d.mts/.d.cts file. The npm case hono-node-server drives buffered, streamed, cookie, header-form, HEAD, error-handler and URL cases against the real package under Node. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + docs/content/docs/dependencies.mdx | 7 + .../compiler/shims/hono-node-server/v2.ts | 479 ++++++ .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-calls.ts | 1 + .../compiler/src/frontend/package-shims.ts | 15 +- .../compiler/src/ir/builtin-signatures.ts | 1 + .../compiler/test/package-shim-guard.test.ts | 1 + .../npm/cases/hono-node-server/main.ts.json | 7 + packages/runtime/src/scr_fetch.c | 162 +- packages/runtime/src/scr_generator.h | 1 + .../npm/cases/hono-node-server/main.ts | 238 +++ .../npm/cases/hono-node-server/package.json | 1 + .../node_modules/@hono/node-server/LICENSE | 21 + .../node_modules/@hono/node-server/README.md | 450 ++++++ .../@hono/node-server/dist/conninfo.cjs | 22 + .../@hono/node-server/dist/conninfo.d.cts | 11 + .../@hono/node-server/dist/conninfo.d.mts | 11 + .../@hono/node-server/dist/conninfo.mjs | 20 + .../node-server/dist/constants-BLSFu_RU.mjs | 5 + .../node-server/dist/constants-BXAKTxRC.cjs | 11 + .../@hono/node-server/dist/early-hints.cjs | 35 + .../@hono/node-server/dist/early-hints.d.cts | 16 + .../@hono/node-server/dist/early-hints.d.mts | 16 + .../@hono/node-server/dist/early-hints.mjs | 33 + .../@hono/node-server/dist/index.cjs | 1340 +++++++++++++++++ .../@hono/node-server/dist/index.d.cts | 101 ++ .../@hono/node-server/dist/index.d.mts | 101 ++ .../@hono/node-server/dist/index.mjs | 1335 ++++++++++++++++ .../@hono/node-server/dist/serve-static.cjs | 153 ++ .../@hono/node-server/dist/serve-static.d.cts | 19 + .../@hono/node-server/dist/serve-static.d.mts | 19 + .../@hono/node-server/dist/serve-static.mjs | 152 ++ .../@hono/node-server/dist/utils/response.cjs | 8 + .../node-server/dist/utils/response.d.cts | 4 + .../node-server/dist/utils/response.d.mts | 4 + .../@hono/node-server/dist/utils/response.mjs | 7 + .../@hono/node-server/dist/utils/stream.cjs | 65 + .../@hono/node-server/dist/utils/stream.d.cts | 6 + .../@hono/node-server/dist/utils/stream.d.mts | 6 + .../@hono/node-server/dist/utils/stream.mjs | 64 + .../@hono/node-server/package.json | 128 ++ tests/fixtures/npm/node_modules/hono/LICENSE | 21 + .../hono/dist/helper/websocket/index.js | 53 + .../npm/node_modules/hono/package.json | 12 + 45 files changed, 5158 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/shims/hono-node-server/v2.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json create mode 100644 tests/fixtures/npm/cases/hono-node-server/main.ts create mode 100644 tests/fixtures/npm/cases/hono-node-server/package.json create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/LICENSE create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/README.md create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/package.json create mode 100644 tests/fixtures/npm/node_modules/hono/LICENSE create mode 100644 tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/package.json diff --git a/.gitignore b/.gitignore index cd44fe11d..b271af8e1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ output/ /packages/runtime-*/runtime-pack.json /packages/runtime-*/.runtime-pack-* !tests/fixtures/npm/node_modules/*/dist/ +!tests/fixtures/npm/node_modules/@*/*/dist/ !tests/fixtures/npm/workspace/*/dist/ !tests/fixtures/npm/cases/workspace-copied/members/*/dist/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 409b9cb30..3098dcad4 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -92,6 +92,13 @@ scriptc implements a few packages itself and compiles that implementation in pla - graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying. - An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there. +`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies are read before the handler runs. Known differences: + +- Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files. +- The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`. +- The global `Request` and `Response` stay the built-in classes, so `overrideGlobalObjects` has no effect, and a `Response` reads its default text content type as `text/plain;charset=UTF-8` where the package's replacement reads `text/plain; charset=UTF-8`. Written responses carry the package's spelling. +- A streamed response is written without waiting for `drain`, and a failed handler answers 504 only for an error whose `name` is `TimeoutError`. + Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled. diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts new file mode 100644 index 000000000..3708f0e4c --- /dev/null +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -0,0 +1,479 @@ +// scriptc's built-in implementation of `@hono/node-server` 2.x: the Node.js +// adapter that serves a fetch-style handler (Hono's `app.fetch`) over +// node:http. The published package swaps the global Request and Response +// for lightweight objects and reparents prototypes, which only pays off +// under V8; this port builds native Request objects from the incoming +// message and writes the handler's Response the way the package's +// listener does (header record, content length for bodies that complete +// within the first reads, streamed bodies, error responses), so a client +// sees the same status lines, headers and bodies. +// +// HTTP/1.1 only: the http2/https server options and WebSocket upgrades are +// not declared. A Response that is still exactly its constructor arguments +// takes the package's cached-response path (its lightweight Response), read +// from the runtime through scriptcResponseLight; every other Response takes +// the general path. +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import type { AddressInfo } from "node:net"; + +export type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; + +export type ServerType = Server; + +export type FetchCallback = (request: Request, env: HttpBindings) => Response | Promise; + +export type CustomErrorHandler = (err: unknown) => void | Response | Promise; + +export type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; +}; + +export interface RequestListenerOptions { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +} + +export class RequestError extends Error { + constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "RequestError"; + } +} + +const DEFAULT_CONTENT_TYPE = "text/plain; charset=UTF-8"; +const X_ALREADY_SENT = "x-hono-already-sent"; + +const reValidRequestUrl = /^\/[!#$&-;=?-[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; + +function buildUrl(scheme: string, host: string, incomingUrl: string): string { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if ( + urlObj.hostname.length !== host.length && + urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase() + ) + throw new RequestError("Invalid host header"); + return urlObj.href; + } + if (incomingUrl.length === 0) return url + "/"; + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) + return new URL(url).href; + return url; +} + +function normalizeIncomingMethod(method: string | undefined): string { + if (method === undefined || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": + return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": + return upper; + default: + return method; + } +} + +/** The request URL the package builds: an absolute request target as is, + * otherwise the Host header (or the configured hostname) over http. */ +function requestUrl(incoming: IncomingMessage, defaultHostname: string | undefined): string { + const incomingUrl = incoming.url ?? ""; + if ( + incomingUrl[0] !== "/" && + (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://")) + ) { + try { + return new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + } + const hostHeader = incoming.headers.host; + const host = hostHeader !== undefined && hostHeader !== "" ? hostHeader : (defaultHostname ?? ""); + if (host === "") throw new RequestError("Missing host header"); + try { + return buildUrl("http", host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + throw new RequestError("Invalid URL", { cause: e }); + } +} + +function requestHeaders(incoming: IncomingMessage): Headers { + const headers = new Headers(); + const raw = incoming.rawHeaders; + for (let i = 0; i < raw.length; i += 2) { + const name = raw[i]!; + if (!name.startsWith(":")) headers.append(name, raw[i + 1]!); + } + return headers; +} + +function readIncoming(incoming: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + incoming.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + incoming.on("end", () => resolve(chunks.length === 1 ? chunks[0]! : Buffer.concat(chunks))); + incoming.on("error", reject); + }); +} + +async function newRequest( + incoming: IncomingMessage, + defaultHostname: string | undefined, +): Promise { + const method = normalizeIncomingMethod(incoming.method); + const url = requestUrl(incoming, defaultHostname); + const headers = requestHeaders(incoming); + if (method === "GET" || method === "HEAD") return new Request(url, { method, headers }); + const body = await readIncoming(incoming); + return new Request(url, { method, headers, body }); +} + +/** The package's outgoing header record: Headers order (lowercase, + * sorted), Set-Cookie values gathered into one array entry at the end, + * and the default content type for a response with a body. */ +function buildOutgoingHttpHeaders( + headers: Headers, + hasBody: boolean, +): Record { + const res: Record = {}; + if (headers.has("set-cookie")) { + const cookies: string[] = []; + headers.forEach((v, k) => { + if (k === "set-cookie") cookies.push(v); + else res[k] = v; + }); + // Headers.forEach joins nothing but set-cookie, which getSetCookie + // lists value by value, as the package's iteration does. + if (cookies.length > 0) res["set-cookie"] = headers.getSetCookie(); + } else { + headers.forEach((v, k) => { + res[k] = v; + }); + } + if (hasBody && res["content-type"] === undefined) res["content-type"] = DEFAULT_CONTENT_TYPE; + return res; +} + +type ReadResult = ReadableStreamReadResult; + +function readWithoutBlocking(read: Promise): Promise { + return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]); +} + +function nextTick(): Promise { + return new Promise((resolve) => { + setTimeout(resolve); + }); +} + +async function pump( + reader: ReadableStreamDefaultReader, + outgoing: ServerResponse, + pending: Promise | undefined, +): Promise { + let read = pending ?? reader.read(); + for (;;) { + const { done, value } = await read; + if (done) { + outgoing.end(); + return; + } + outgoing.write(value); + read = reader.read(); + } +} + +function handleResponseError(e: unknown, outgoing: ServerResponse): void { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + console.error(e); + if (!outgoing.headersSent) outgoing.writeHead(500, { "Content-Type": "text/plain" }); + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(); +} + +function emptyResponse(status: number): Response { + return new Response(null, { status }); +} + +/** The lightweight state of a constructed Response (the runtime's + * scr_fetch_response_light): [form, flags, byte length, ...record pairs]. */ +declare function scriptcResponseLight(res: Response): string[]; + +const UNDICI_TEXT_TYPE = "text/plain;charset=UTF-8"; + +/** Content-Length as Node's implicit header: set the headers one by one and + * let end(body) compute the length, which Node then writes after its own + * Date/Connection headers, as the package's preset length does. */ +function endWithImplicitLength( + outgoing: ServerResponse, + status: number, + header: Record, + body: Uint8Array, +): void { + outgoing.statusCode = status; + for (const name of Object.keys(header)) outgoing.setHeader(name, header[name]!); + outgoing.end(body); +} + +/** The package's cached-response path: a Response that is still exactly + * its constructor arguments (no body or metadata read since) is written + * from them. Form 1 has no init headers, form 2 Headers, form 3 a plain + * record kept verbatim. */ +async function writeLightResponse( + res: Response, + outgoing: ServerResponse, + light: string[], + head: boolean, +): Promise { + const form = light[0]!; + const flags = light[1]!; + const length = Number(light[2]!); + const status = res.status; + const sized = length >= 0; + if (form === "1") { + if (length === -1) { + outgoing.writeHead(status); + outgoing.end(); + } else if (sized) { + const body = await res.bytes(); + if (head) { + outgoing.writeHead(status, { + "Content-Type": DEFAULT_CONTENT_TYPE, + "Content-Length": String(length), + }); + outgoing.end(body); + } else + endWithImplicitLength(outgoing, status, { "Content-Type": DEFAULT_CONTENT_TYPE }, body); + } else { + outgoing.writeHead(status, { "Content-Type": DEFAULT_CONTENT_TYPE }); + outgoing.flushHeaders(); + await pump(res.body!.getReader(), outgoing, undefined); + } + return; + } + let header: Record; + let hasContentLength = false; + let plain = false; + let canAutoLength = true; + if (form === "2") { + const headers = new Headers(res.headers); + if (flags.includes("a") && headers.get("content-type") === UNDICI_TEXT_TYPE) { + if (flags.includes("d")) headers.set("content-type", DEFAULT_CONTENT_TYPE); + else headers.delete("content-type"); + } else if (flags.includes("d") && !headers.has("content-type")) { + headers.set("content-type", DEFAULT_CONTENT_TYPE); + } + hasContentLength = headers.has("content-length"); + header = buildOutgoingHttpHeaders(headers, length !== -1); + } else { + plain = true; + header = {}; + for (let i = 3; i + 1 < light.length; i += 2) { + const name = light[i]!; + const value = light[i + 1]!; + const previous = header[name]; + if (previous === undefined) header[name] = value; + else if (typeof previous === "string") header[name] = [previous, value]; + else previous.push(value); + const lower = name.toLowerCase(); + if (lower === "content-length") hasContentLength = true; + else if (lower === "transfer-encoding" || lower === "trailer") canAutoLength = false; + } + } + const body = sized ? await res.bytes() : undefined; + if (!hasContentLength && body !== undefined) { + if (plain && canAutoLength && !head && status !== 204 && status !== 304) { + endWithImplicitLength(outgoing, status, header, body); + return; + } + header["Content-Length"] = String(length); + } + outgoing.writeHead(status, header); + if (length === -1) outgoing.end(); + else if (body !== undefined) outgoing.end(body); + else { + outgoing.flushHeaders(); + await pump(res.body!.getReader(), outgoing, undefined); + } +} + +async function writeResponse( + res: Response, + outgoing: ServerResponse, + head: boolean, +): Promise { + const light = scriptcResponseLight(res); + if (light[0] !== "0") return writeLightResponse(res, outgoing, light, head); + const body = res.body; + const record = buildOutgoingHttpHeaders(res.headers, body !== null); + if (body !== null) { + const reader = body.getReader(); + const values: Uint8Array[] = []; + let done = false; + let pending: Promise | undefined = undefined; + if (record["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + pending ??= reader.read(); + let chunk: ReadResult | undefined = undefined; + try { + chunk = await readWithoutBlocking(pending); + } catch (e) { + console.error(e); + done = true; + } + if (chunk === undefined) { + if (i === 1 && !done) { + await nextTick(); + maxReadCount = 3; + continue; + } + break; + } + pending = undefined; + if (chunk.value !== undefined) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && record["content-length"] === undefined) { + let length = 0; + for (const value of values) length += value.length; + record["content-length"] = String(length); + } + } + outgoing.writeHead(res.status, record); + for (const value of values) outgoing.write(value); + if (done) outgoing.end(); + else { + if (values.length === 0) outgoing.flushHeaders(); + await pump(reader, outgoing, pending); + } + } else if (record[X_ALREADY_SENT] === undefined) { + outgoing.writeHead(res.status, record); + outgoing.end(); + } +} + +async function respond( + pending: Response | Promise, + outgoing: ServerResponse, + errorHandler: CustomErrorHandler | undefined, + head: boolean, +): Promise { + let res: Response; + if (pending instanceof Promise) { + try { + res = await pending; + } catch (err) { + if (errorHandler === undefined) { + res = handleFetchError(err); + } else { + const errRes = await errorHandler(err); + if (errRes === undefined) return; + res = errRes; + } + } + } else res = pending; + await writeResponse(res, outgoing, head); +} + +function handleFetchError(e: unknown): Response { + return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500); +} + +async function handleRequest( + fetchCallback: FetchCallback, + options: RequestListenerOptions, + incoming: IncomingMessage, + outgoing: ServerResponse, +): Promise { + let req: Request | undefined = undefined; + let res: Response | Promise | undefined = undefined; + try { + req = await newRequest(incoming, options.hostname); + res = fetchCallback(req, { incoming, outgoing }); + } catch (e) { + if (res !== undefined) return handleResponseError(e, outgoing); + const errorHandler = options.errorHandler; + if (errorHandler !== undefined) { + const handled = await errorHandler( + req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e), + ); + if (handled === undefined) return; + res = handled; + } else if (req === undefined) res = emptyResponse(400); + else res = handleFetchError(e); + } + try { + await respond(res, outgoing, options.errorHandler, incoming.method === "HEAD"); + } catch (e) { + handleResponseError(e, outgoing); + } +} + +function toRequestError(e: unknown): RequestError { + return new RequestError(e instanceof Error ? e.message : String(e), { cause: e }); +} + +export function getRequestListener( + fetchCallback: FetchCallback, + options: RequestListenerOptions = {}, +): (incoming: IncomingMessage, outgoing: ServerResponse) => Promise { + return (incoming, outgoing) => handleRequest(fetchCallback, options, incoming, outgoing); +} + +export function createAdaptorServer(options: Options): ServerType { + const listener = getRequestListener(options.fetch, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming, + }); + return createServer((incoming, outgoing) => { + void listener(incoming, outgoing); + }); +} + +export function serve( + options: Options, + listeningListener?: (info: AddressInfo) => void, +): ServerType { + const server = createAdaptorServer(options); + server.listen(options.port ?? 3000, options.hostname, () => { + const info = server.address() as AddressInfo; + if (listeningListener !== undefined) listeningListener(info); + }); + return server; +} diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..8c7d44cdb 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -893,6 +893,7 @@ export const LIB_FN_SYMS: Record = { // ambient-this read never throw. The fs dyn read is the sync-fs story. "fetch.start": "scr_fetch_static", "fetch.responseNew": "scr_fetch_response_new", + "fetch.responseLight": "scr_fetch_response_light", "fetch.responseJsonStatic": "scr_fetch_response_json_static", "fetch.input": "scr_fetch_input", "abort.controllerNew": "scr_fetch_abort_controller_new", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c1bc044ae..c7332016c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -11157,6 +11157,7 @@ const SHIM_INTRINSICS: ReadonlyMap = new Map< variants: ["hot-shots/v8-unix-dgram.ts"], }, ], + [ + "@hono/node-server", + { + name: "@hono/node-server", + mainFile: "dist/index.mjs", + typesFile: "dist/index.d.mts", + versions: new Map([[2, "hono-node-server/v2.ts"]]), + }, + ], [ "undici", { @@ -324,7 +333,7 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean { /** The shimmed package whose declaration entry `path` is, with the * package directory: `/index.d.ts`, or the entry's own `typesFile`. */ function shimOfDeclarationPath(path: string): { pkgDir: string; shim: ImplementationShim } | null { - if (!path.endsWith(".d.ts")) return null; + if (!/\.d\.[cm]?ts$/.test(path)) return null; if (path.endsWith("/index.d.ts")) { const pkgDir = dirname(path); const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1)); @@ -377,7 +386,7 @@ export function isPackageShimFile(fileName: string): boolean { /** True for a name a shipped shim may declare as a bodiless runtime * intrinsic (lower-calls.ts lowers its calls to runtime calls). */ export function isShimIntrinsicName(name: string): boolean { - return name.startsWith("scriptcUnixDgram"); + return name.startsWith("scriptcUnixDgram") || name === "scriptcResponseLight"; } /** Resets per-compile caches. */ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..8d2de2a87 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -66,6 +66,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "abort.controllerNew": { argTypes: [], result: DYN }, /** EventTarget / Event / CustomEvent construction (scr_event_target.c): diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts index a8620c1ce..9843739cd 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -51,6 +51,7 @@ describe("package shims compile without refusals", () => { expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ "chalk/v5.ts", "fs-extra/index.ts", + "hono-node-server/v2.ts", "hot-shots/v8-unix-dgram.ts", "hot-shots/v8.ts", "undici/v5.ts", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json new file mode 100644 index 000000000..58f450083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/packages/compiler/shims/hono-node-server/v2.ts", + "/tests/fixtures/npm/cases/hono-node-server/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..18daed32f 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -184,6 +184,23 @@ struct SfResponse { int status; bool redirected; bool null_body; + /* @hono/node-server's lightweight-response state (scr_fetch_response_light): + * whether a constructed Response is still one the package would write + * from its constructor arguments, and in which header form. 0: not (it + * was materialized by a body/metadata read, or never qualified); 1: no + * init headers; 2: a Headers (or pair list) form; 3: a plain record, + * whose raw entries light_record keeps in order. */ + int8_t light; + /* Form 2 reached from form 1 by a headers read: the package's Headers + * start with its default text content type when the body is not null. */ + bool light_default_ct; + /* The constructor added the text content type (a string body without + * one), which the package's lightweight headers never hold. */ + bool auto_ct; + /* The body's byte length (string or Uint8Array), -1 for a null body, + * -2 for a stream. */ + double light_length; + ScrArr *light_record; }; #ifndef SCR_FETCH_SIGNAL_ONLY @@ -2627,6 +2644,7 @@ static void sf_response_release(SfResponse *r) { sf_headers_release(r->headers); scr_str_release(r->url); scr_str_release(r->status_text); + scr_arr_release(r->light_record); free(r); } @@ -2637,6 +2655,13 @@ static void sf_response_release_v(void *p) { sf_response_release((SfResponse *)p); } +/* A read the package's lightweight Response answers from its native + * response materializes it; status, ok and headers keep it light, and a + * headers read turns the header form into a Headers object. */ +static void sf_response_materialize(SfResponse *r) { + r->light = 0; +} + static ScrPromise *sf_response_collect(SfResponse *r, int mode) { if (!r->null_body) return sf_stream_collect(r->body, mode); @@ -2675,6 +2700,7 @@ static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self, ScrDyn *const *args, size_t argc, const char *what) { SfResponse *r = ptr; + sf_response_materialize(r); int mode = -1; if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON; else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT; @@ -2698,6 +2724,19 @@ static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "ok")) { return scr_dyn_new_bool(r->status >= 200 && r->status <= 299); } + if (sf_name(key, len, "headers")) { + if (r->light == 1) { + r->light = 2; + r->light_default_ct = !r->null_body; + } else if (r->light == 3) { + r->light = 2; + } + return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS); + } + if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || + sf_name(key, len, "url") || sf_name(key, len, "redirected") || + sf_name(key, len, "statusText")) + sf_response_materialize(r); if (sf_name(key, len, "bodyUsed")) { return scr_dyn_new_bool(!r->null_body && r->body->disturbed); } @@ -2713,9 +2752,6 @@ static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "statusText")) { return scr_dyn_new_str(r->status_text); } - if (sf_name(key, len, "headers")) { - return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS); - } return NULL; } @@ -2727,6 +2763,7 @@ static ScrPromise *sf_response_collect_dyn(ScrDyn *response, int mode) { &scr_dyn_release_v, NULL); } SfResponse *r = response->v.handle.ptr; + sf_response_materialize(r); return sf_response_collect(r, mode); } @@ -4719,6 +4756,61 @@ static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { return serialized; } +/* The lightweight header form of a constructor's init headers (see + * SfResponse.light): absent headers are form 1 (Response.json's plain + * {"content-type": "application/json"} record is form 3), a Headers object + * or pair list form 2, and a plain record form 3, whose names and values + * *record receives in order (an array value contributes one entry per + * element, as writeHead writes it). 0 for any other init. */ +static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, + ScrArr **record) { + if (!headers || headers->kind == SCR_DYN_UNDEF) { + if (!json_body) return 1; + *record = scr_arr_new(SCR_ELEM_STR, 2); + sf_push_header_text(*record, "content-type", "application/json", 16); + return 3; + } + if (headers->kind == SCR_DYN_ARR || + (headers->kind == SCR_DYN_HANDLE && + headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS)) + return 2; + ScrDyn *materialized = NULL; + if (headers->kind == SCR_DYN_TYPED_REF) { + materialized = scr_dyn_typed_ref_materialize(headers); + if (!materialized) return 0; + headers = materialized; + } + int8_t form = 0; + if (headers->kind == SCR_DYN_OBJ) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * headers->v.obj.len); + form = 3; + for (size_t i = 0; i < headers->v.obj.len && form == 3; i++) { + const ScrDynEntry *e = &headers->v.obj.entries[i]; + const ScrDyn *value = e->value; + size_t count = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1; + for (size_t j = 0; j < count; j++) { + const ScrDyn *item = value->kind == SCR_DYN_ARR ? value->v.arr.items[j] : value; + if (item->kind != SCR_DYN_STR && item->kind != SCR_DYN_NUM && + item->kind != SCR_DYN_BOOL) { + form = 0; + break; + } + ScrStr *text = scr_dyn_string_coerce_js(item); + if (!text) { + form = 0; + break; + } + scr_arr_push_ref(out, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(out, text); + } + } + if (form == 3) *record = out; + else scr_arr_release(out); + } + scr_dyn_release(materialized); + return form; +} + static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, const ScrDyn *json_data, SfJsonSerialize serialize) { @@ -4733,6 +4825,23 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, bool text_body = false; int status = 200; bool status_range_error = false; + /* The lightweight-response classification (see SfResponse.light): a + * Response init qualifies only while it is itself still light. */ + bool light_ok = !(init && init->kind == SCR_DYN_HANDLE && + init->v.handle.tag == SCR_DYNH_FETCH_RESPONSE && + ((SfResponse *)init->v.handle.ptr)->light == 0); + if (!json_body && !null_body) { + if (body->kind == SCR_DYN_BYTES) + light_ok = light_ok && body->v.bytes->elem == SCR_BYTES_U8 && + !body->v.bytes->is_data_view; + else if (!(body->kind == SCR_DYN_STR || + (body->kind == SCR_DYN_HANDLE && + body->v.handle.tag == SCR_DYNH_WEB_STREAM))) + light_ok = false; + } + int8_t light_form = 0; + ScrArr *light_record = NULL; + size_t light_length_bytes = 0; /* WebIDL converts constructor arguments from left to right. BodyInit's * string arm therefore runs before ResponseInit's dictionary conversion, @@ -4770,6 +4879,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, * coercions precede status and statusText. Header syntax validation is a * later response-initialization step, after body extraction. */ ScrDyn *init_headers = sf_response_init_get(init, "headers", 7); + if (light_ok) light_form = sf_light_header_form(init_headers, json_body, &light_record); converted_headers = sf_headers_convert_init(init_headers); scr_dyn_release(init_headers); if (!converted_headers) goto fail; @@ -4826,6 +4936,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, scr_str_release(encoding); scr_dyn_release(serialized); stream = sf_stream_new_native(); + light_length_bytes = body_bytes->len; sf_stream_enqueue_bytes(stream, body_bytes); sf_stream_close(stream); scr_bytes_release(body_bytes); @@ -4844,6 +4955,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, sf_stream_retain(stream); } else { stream = sf_stream_new_native(); + light_length_bytes = body_bytes->len; sf_stream_enqueue_bytes(stream, body_bytes); sf_stream_close(stream); scr_bytes_release(body_bytes); @@ -4877,9 +4989,11 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, sf_type_error(message); goto fail; } + bool auto_ct = false; if (text_body && !sf_pairs_have(header_pairs, "content-type")) { sf_push_header_text(header_pairs, "content-type", "text/plain;charset=UTF-8", 24); + auto_ct = true; } if (json_body && !sf_pairs_have(header_pairs, "content-type")) { static const char json_type[16] = "application/json"; @@ -4897,6 +5011,14 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, response->status = status; response->redirected = false; response->null_body = null_body; + response->light = light_form; + response->auto_ct = auto_ct; + response->light_record = light_record; + light_record = NULL; + response->light_length = + null_body ? -1.0 + : (!json_body && body->kind == SCR_DYN_HANDLE) ? -2.0 + : (double)light_length_bytes; ScrDyn *out = scr_dyn_new_handle(response, SCR_DYNH_FETCH_RESPONSE); sf_response_release(response); @@ -4908,9 +5030,43 @@ fail: scr_str_release(status_text); scr_arr_release(header_pairs); scr_arr_release(converted_headers); + scr_arr_release(light_record); return NULL; } +/* @hono/node-server's built-in implementation reads a Response's + * lightweight state to write it as the package does: [form, flags, + * byte length, ...raw record name/value pairs], with form as in + * SfResponse.light ("0" alone when the response is not light), flags "d" + * (the package's Headers hold its default content type) and "a" (the + * constructor added the text content type), and the length -1 for a null + * body or -2 for a stream. */ +ScrArr *scr_fetch_response_light(ScrDyn *response) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 4); + if (!response || response->kind != SCR_DYN_HANDLE || + response->v.handle.tag != SCR_DYNH_FETCH_RESPONSE) { + scr_arr_push_ref(out, scr_str_new("0", 1)); + return out; + } + SfResponse *r = response->v.handle.ptr; + char buf[32]; + int n = snprintf(buf, sizeof buf, "%d", (int)r->light); + scr_arr_push_ref(out, scr_str_new(buf, (size_t)n)); + if (r->light == 0) return out; + char flags[3]; + size_t f = 0; + if (r->light_default_ct) flags[f++] = 'd'; + if (r->auto_ct) flags[f++] = 'a'; + scr_arr_push_ref(out, scr_str_new(flags, f)); + n = snprintf(buf, sizeof buf, "%.0f", r->light_length); + scr_arr_push_ref(out, scr_str_new(buf, (size_t)n)); + if (r->light == 3 && r->light_record) { + for (size_t i = 0; i < r->light_record->len; i++) + scr_arr_push_ref(out, scr_arr_get_ref(r->light_record, (double)i)); + } + return out; +} + ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { return sf_response_construct(body, init, NULL, NULL); } diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c3dac861d..828f339d5 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -181,6 +181,7 @@ ScrDyn *scr_fetch_headers_new(ScrDyn *init); ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */ +ScrArr *scr_fetch_response_light(ScrDyn *response); /* @hono/node-server shim: borrowed; +1 string[] */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ diff --git a/tests/fixtures/npm/cases/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts new file mode 100644 index 000000000..ed732f64e --- /dev/null +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -0,0 +1,238 @@ +// @hono/node-server 2.x (compiled from scriptc's built-in implementation) +// serving a fetch handler: a raw HTTP client prints each exchange's status +// line, wire-order headers (Date dropped) and body, covering buffered and +// streamed bodies, header forms, cookies, request bodies, URL handling, +// HEAD, empty statuses, and handler errors. +import * as http from "node:http"; +import type { AddressInfo } from "node:net"; +import { getRequestListener, RequestError, serve } from "@hono/node-server"; + +function delay(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function slowStream(parts: string[]): ReadableStream { + const encoder = new TextEncoder(); + let index = 0; + return new ReadableStream({ + async pull(controller) { + await delay(5); + if (index < parts.length) controller.enqueue(encoder.encode(parts[index++]!)); + else controller.close(); + }, + }); +} + +function readyStream(parts: string[]): ReadableStream { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + for (const part of parts) controller.enqueue(encoder.encode(part)); + controller.close(); + }, + }); +} + +async function handler(request: Request): Promise { + const url = new URL(request.url); + switch (url.pathname) { + case "/echo": { + const body = await request.text(); + const headers = new Headers({ "content-type": "application/json" }); + headers.set("x-echo", "yes"); + return new Response( + JSON.stringify({ + method: request.method, + url: request.url.replace(/:\d+\//, ":PORT/"), + type: request.headers.get("content-type"), + agent: request.headers.get("x-agent"), + body, + }), + { status: 201, headers }, + ); + } + case "/headers-object": { + const headers = new Headers(); + headers.set("x-b", "2"); + headers.set("X-A", "1"); + headers.set("content-type", "text/html; charset=utf-8"); + return new Response("

hi

", { status: 202, headers }); + } + case "/cookies": { + const headers = new Headers(); + headers.append("set-cookie", "a=1; Path=/"); + headers.set("x-before", "b"); + headers.append("set-cookie", "b=2; HttpOnly"); + headers.set("content-type", "text/plain"); + return new Response("cookies", { headers }); + } + case "/no-content": + return new Response(null, { status: 204, headers: new Headers({ "x-empty": "1" }) }); + case "/declared-length": { + const headers = new Headers({ "content-length": "5", "content-type": "text/plain" }); + return new Response("12345", { headers }); + } + case "/ready-stream": { + const headers = new Headers({ "content-type": "text/plain" }); + return new Response(readyStream(["one ", "two"]), { headers }); + } + case "/slow-stream": { + const headers = new Headers({ "content-type": "application/x-ndjson" }); + return new Response(slowStream(['{"n":1}\n', '{"n":2}\n', '{"n":3}\n']), { headers }); + } + case "/chunked-stream": { + const headers = new Headers({ "content-type": "text/event-stream", "transfer-encoding": "chunked" }); + return new Response(readyStream(["data: a\n\n", "data: b\n\n"]), { headers }); + } + case "/bytes": { + const headers = new Headers({ "content-type": "application/octet-stream" }); + return new Response(new Uint8Array([104, 105, 33]), { headers }); + } + case "/null-body": { + return new Response(null, { status: 404, headers: new Headers({ "x-missing": "yes" }) }); + } + case "/reject": + throw new Error("handler failed"); + case "/light-text": + return new Response("light"); + case "/light-record": + return new Response("rec", { status: 201, headers: { "X-Custom": "a", "Content-Type": "text/html" } }); + case "/light-json": + return Response.json({ ok: true, n: [1, 2] }); + case "/light-touched": { + const r = new Response("touched"); + r.headers.set("x-touched", "1"); + return r; + } + case "/light-record-touched": { + const r = new Response("rt", { headers: { "X-Rec": "1" } }); + r.headers.append("x-more", "2"); + return r; + } + case "/materialized": { + const r = new Response("m", { headers: { "X-M": "1" } }); + return new Response(r.body, r); + } + case "/light-stream": + return new Response(readyStream(["s1 ", "s2"])); + case "/light-null": + return new Response(null, { status: 202 }); + case "/light-length": + return new Response("abc", { headers: { "Content-Length": "3" } }); + case "/light-bytes": + return new Response(new Uint8Array([111, 107])); + default: { + const headers = new Headers({ "content-type": "text/plain; charset=UTF-8" }); + return new Response(`path ${url.pathname}${url.search}`, { headers }); + } + } +} + +interface Exchange { + method: string; + path: string; + headers?: Record; + body?: string; +} + +function exchange(port: number, ex: Exchange): Promise { + return new Promise((resolve, reject) => { + const headers: Record = { ...(ex.headers ?? {}) }; + if (ex.body !== undefined) headers["content-length"] = String(Buffer.byteLength(ex.body)); + const req = http.request( + { host: "127.0.0.1", port, method: ex.method, path: ex.path, headers: headers }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + res.on("end", () => { + const lines: string[] = [`HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`]; + for (let i = 0; i < res.rawHeaders.length; i += 2) { + const name = res.rawHeaders[i]!; + if (name.toLowerCase() === "date") continue; + lines.push(`${name}: ${res.rawHeaders[i + 1]}`); + } + resolve(`### ${ex.method} ${ex.path}\n${lines.join("\n")}\n\n${Buffer.concat(chunks).toString("utf8")}\n`); + }); + }, + ); + req.on("error", reject); + if (ex.body !== undefined) req.write(ex.body); + req.end(); + }); +} + +const exchanges: Exchange[] = [ + { method: "GET", path: "/plain?q=1" }, + { method: "POST", path: "/echo", headers: { "content-type": "text/plain", "x-agent": "probe" }, body: "payload body" }, + { method: "PUT", path: "/echo", headers: { "content-type": "application/json" }, body: '{"k":[1,2]}' }, + { method: "GET", path: "/headers-object" }, + { method: "GET", path: "/cookies" }, + { method: "GET", path: "/no-content" }, + { method: "GET", path: "/declared-length" }, + { method: "GET", path: "/ready-stream" }, + { method: "GET", path: "/slow-stream" }, + { method: "GET", path: "/chunked-stream" }, + { method: "GET", path: "/bytes" }, + { method: "GET", path: "/null-body" }, + { method: "HEAD", path: "/plain" }, + { method: "GET", path: "/light-text" }, + { method: "HEAD", path: "/light-text" }, + { method: "GET", path: "/light-record" }, + { method: "HEAD", path: "/light-record" }, + { method: "GET", path: "/light-json" }, + { method: "GET", path: "/light-touched" }, + { method: "GET", path: "/light-record-touched" }, + { method: "GET", path: "/materialized" }, + { method: "GET", path: "/light-stream" }, + { method: "GET", path: "/light-null" }, + { method: "GET", path: "/light-length" }, + { method: "GET", path: "/light-bytes" }, + { method: "GET", path: "/reject" }, + { method: "GET", path: "/a/../b/./c?x=%20y" }, + { method: "GET", path: "/upper", headers: { host: "Example.COM:8080" } }, + { method: "GET", path: "/bad-host", headers: { host: "bad host" } }, + { method: "get", path: "/lower-method" }, +]; + +const server = serve({ fetch: handler, port: 0, hostname: "127.0.0.1" }, (info: AddressInfo) => { + console.log("listening", info.family, info.address, typeof info.port); + void (async () => { + for (const ex of exchanges) console.log(await exchange(info.port, ex)); + server.close(() => { + console.log("closed"); + startSecond(); + }); + })(); +}); + +// getRequestListener with an error handler, on a plain node:http server. +const listener = getRequestListener( + async (request) => { + if (request.url.endsWith("/fail")) throw new RequestError("listener failure"); + return new Response("ok", { headers: new Headers({ "content-type": "text/plain" }) }); + }, + { + errorHandler: (err) => + new Response(`handled: ${err instanceof Error ? `${err.name} ${err.message}` : "?"}`, { + status: 503, + headers: new Headers({ "content-type": "text/plain" }), + }), + }, +); +function startSecond(): void { + const second = http.createServer((req, res) => { + void listener(req, res); + }); + second.listen(0, "127.0.0.1", () => { + const port = (second.address() as AddressInfo).port; + void (async () => { + console.log(await exchange(port, { method: "GET", path: "/ok" })); + console.log(await exchange(port, { method: "GET", path: "/fail" })); + second.close(() => console.log("second closed")); + })(); + }); +} diff --git a/tests/fixtures/npm/cases/hono-node-server/package.json b/tests/fixtures/npm/cases/hono-node-server/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hono-node-server/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/LICENSE b/tests/fixtures/npm/node_modules/@hono/node-server/LICENSE new file mode 100644 index 000000000..5f84afaff --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors + +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/@hono/node-server/README.md b/tests/fixtures/npm/node_modules/@hono/node-server/README.md new file mode 100644 index 000000000..25af664e8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/README.md @@ -0,0 +1,450 @@ +# Node.js Adapter for Hono + +This adapter `@hono/node-server` allows you to run your Hono application on Node.js. +Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. It utilizes web standard APIs implemented in Node.js. + +## Benchmarks + +Hono is 3.1 times faster than Express. + +Express v5: + +```txt +$ bombardier -d 10s --fasthttp http://localhost:3000/ + +Statistics Avg Stdev Max + Reqs/sec 24380.41 1409.74 26604.88 + Latency 5.12ms 2.95ms 305.21ms + HTTP codes: + 1xx - 0, 2xx - 243915, 3xx - 0, 4xx - 0, 5xx - 0 + others - 0 + Throughput: 6.98MB/s +``` + +Hono + `@hono/node-server`: + +```txt +$ bombardier -d 10s --fasthttp http://localhost:3000/ + +Statistics Avg Stdev Max + Reqs/sec 74428.96 3815.27 88228.32 + Latency 1.68ms 621.25us 106.20ms + HTTP codes: + 1xx - 0, 2xx - 744378, 3xx - 0, 4xx - 0, 5xx - 0 + others - 0 + Throughput: 16.82MB/s +``` + +## Requirements + +It works on Node.js versions greater than 20.x. + +## Installation + +You can install it from the npm registry: + +```sh +npm install @hono/node-server +pnpm add @hono/node-server +``` + +## Usage + +Just import `@hono/node-server` at the top and write the code as usual. +The same code that runs on Cloudflare Workers, Deno, and Bun will work. + +```ts +import { serve } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono() +app.get('/', (c) => c.text('Hono meets Node.js')) + +serve(app, (info) => { + console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000 +}) +``` + +## WebSocket + +You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`. +To enable this, install `ws` (and `@types/ws`) in your project, then create and provide a `WebSocketServer` as shown in the example below. + +```ts +import { serve, upgradeWebSocket } from '@hono/node-server' +import { WebSocketServer } from 'ws' +import { Hono } from 'hono' + +const app = new Hono() + +app.get( + '/ws', + upgradeWebSocket(() => ({ + onMessage(event, ws) { + ws.send(event.data) + }, + })) +) + +const wss = new WebSocketServer({ noServer: true }) +serve({ + fetch: app.fetch, + websocket: { server: wss }, +}) +``` + +For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`. + +```sh +ts-node ./index.ts +``` + +Open `http://localhost:3000` with your browser. + +## Options + +### `port` + +```ts +serve({ + fetch: app.fetch, + port: 8787, // Port number, default is 3000 +}) +``` + +### `createServer` + +```ts +import { createServer } from 'node:https' +import fs from 'node:fs' + +//... + +serve({ + fetch: app.fetch, + createServer: createServer, + serverOptions: { + key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'), + cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'), + }, +}) +``` + +### `overrideGlobalObjects` + +The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`. + +```ts +serve({ + fetch: app.fetch, + overrideGlobalObjects: false, +}) +``` + +### `autoCleanupIncoming` + +The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`. + +If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`. + +```ts +serve({ + fetch: app.fetch, + autoCleanupIncoming: false, +}) +``` + +### `websocket` + +provide a websocket server to enable websocket support. + +```ts +import { serve, upgradeWebSocket } from '@hono/node-server' +import { WebSocketServer } from 'ws' + +// ... +const wss = new WebSocketServer({ noServer: true }) + +serve({ + fetch: app.fetch, + websocket: { server: wss }, +}) +``` + +## Middleware + +Most built-in middleware also works with Node.js. +Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking. + +```ts +import { serve } from '@hono/node-server' +import { Hono } from 'hono' +import { prettyJSON } from 'hono/pretty-json' + +const app = new Hono() + +app.get('*', prettyJSON()) +app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' })) + +serve(app) +``` + +## Serve Static Middleware + +Use Serve Static Middleware that has been created for Node.js. + +```ts +import { serveStatic } from '@hono/node-server/serve-static' + +//... + +app.use('/static/*', serveStatic({ root: './' })) +``` + +If using a relative path, `root` will be relative to the current working directory from which the app was started. + +This can cause confusion when running your application locally. + +Imagine your project structure is: + +``` +my-hono-project/ + src/ + index.ts + static/ + index.html +``` + +Typically, you would run your app from the project's root directory (`my-hono-project`), +so you would need the following code to serve the `static` folder: + +```ts +app.use('/static/*', serveStatic({ root: './static' })) +``` + +Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`. + +Only `GET` and `HEAD` requests are served. Requests with other methods are passed to the next middleware. + +### Options + +#### `allowPercentInPath` + +By default, `serveStatic` skips request paths that still contain `%` after routing. This prevents a second decoding step from serving a file under a different path than the one checked by route middleware. Set `allowPercentInPath: true` to keep the previous behavior when serving files whose names contain percent signs. + +If you restrict access to only some paths under `root` (for example, `/static/admin/*`), enabling this option may let requests bypass those restrictions. Apply access control to the same route pattern as `serveStatic` (for example, `/static/*`), before the static middleware: + +```ts +app.use('/static/*', authMiddleware) +app.use('/static/*', serveStatic({ root: './static', allowPercentInPath: true })) +``` + +#### `rewriteRequestPath` + +If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following. + +```ts +app.use( + '/__foo/*', + serveStatic({ + root: './.foojs/', + rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''), + }) +) +``` + +#### `onFound` + +You can specify handling when the requested file is found with `onFound`. + +```ts +app.use( + '/static/*', + serveStatic({ + // ... + onFound: (_path, c) => { + c.header('Cache-Control', `public, immutable, max-age=31536000`) + }, + }) +) +``` + +#### `onNotFound` + +The `onNotFound` is useful for debugging. You can write a handle for when a file is not found. + +```ts +app.use( + '/static/*', + serveStatic({ + root: './non-existent-dir', + onNotFound: (path, c) => { + console.log(`${path} is not found, request to ${c.req.path}`) + }, + }) +) +``` + +#### `precompressed` + +The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file. + +```ts +app.use( + '/static/*', + serveStatic({ + precompressed: true, + }) +) +``` + +## ConnInfo Helper + +You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`. + +```ts +import { getConnInfo } from '@hono/node-server/conninfo' + +app.get('/', (c) => { + const info = getConnInfo(c) // info is `ConnInfo` + return c.text(`Your remote address is ${info.remote.address}`) +}) +``` + +## Accessing Node.js API + +You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + return c.json({ + remoteAddress: c.env.incoming.socket.remoteAddress, + }) +}) + +serve(app) +``` + +The APIs that you can get from `c.env` are as follows. + +```ts +type HttpBindings = { + incoming: IncomingMessage + outgoing: ServerResponse +} + +type Http2Bindings = { + incoming: Http2ServerRequest + outgoing: Http2ServerResponse +} +``` + +## Early Hints Middleware + +You can send HTTP 103 Early Hints to instruct browsers to preload or preconnect resources before the final response is prepared. The middleware is supported under Node.js bindings (HTTP/1.1 and HTTP/2). + +### Usage + +Import `earlyHints` from `@hono/node-server/early-hints`: + +#### Static links + +```ts +import { serve } from '@hono/node-server' +import { earlyHints } from '@hono/node-server/early-hints' +import { Hono } from 'hono' + +const app = new Hono() + +app.use( + earlyHints({ + link: '; rel=preload; as=style', + }) +) + +app.get('/', (c) => { + return c.html('

Hello Hono!

') +}) + +serve(app) +``` + +#### Dynamic links + +```ts +app.use( + earlyHints({ + link: (c) => + c.req.query('theme') === 'dark' + ? '; rel=preload; as=style' + : '; rel=preload; as=style', + }) +) +``` + +> [!NOTE] +> Early Hints are sent only for requests that look like document navigations. If `Sec-Fetch-Mode` or `Sec-Fetch-Dest` is present with a value other than `navigate` or `document`, for example a `fetch()` or XHR call from a browser, a subresource request, or an iframe navigation, the middleware skips the hints and continues to the handler. Requests without these headers, such as `curl` or `fetch()` from a JavaScript runtime, are treated as navigations and do receive Early Hints. + +## Direct response from Node.js API + +You can directly respond to the client from the Node.js API. +In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`. + +> [!NOTE] +> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + const { outgoing } = c.env + outgoing.writeHead(200, { 'Content-Type': 'text/plain' }) + outgoing.end('Hello World\n') + + return RESPONSE_ALREADY_SENT +}) + +serve(app) +``` + +## Listen to a UNIX domain socket + +You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port. + +```ts +import { createAdaptorServer } from '@hono/node-server' + +// ... + +const socketPath = '/tmp/example.sock' + +const server = createAdaptorServer(app) +server.listen(socketPath, () => { + console.log(`Listening on ${socketPath}`) +}) +``` + +## Related projects + +- Hono - +- Hono GitHub repository - + +## Authors + +- Yusuke Wada +- Taku Amano + +## License + +MIT diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs new file mode 100644 index 000000000..b41116486 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs @@ -0,0 +1,22 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +//#region src/conninfo.ts +/** +* ConnInfo Helper for Node.js +* @param c Context +* @returns ConnInfo +*/ +const getConnInfo = (c) => { + const bindings = c.env.server ? c.env.server : c.env; + const address = bindings.incoming.socket.remoteAddress; + const port = bindings.incoming.socket.remotePort; + const family = bindings.incoming.socket.remoteFamily; + return { remote: { + address, + port, + addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0 + } }; +}; + +//#endregion +exports.getConnInfo = getConnInfo; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts new file mode 100644 index 000000000..ed43a48f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts @@ -0,0 +1,11 @@ +import { GetConnInfo } from "hono/conninfo"; + +//#region src/conninfo.d.ts +/** + * ConnInfo Helper for Node.js + * @param c Context + * @returns ConnInfo + */ +declare const getConnInfo: GetConnInfo; +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts new file mode 100644 index 000000000..ed43a48f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts @@ -0,0 +1,11 @@ +import { GetConnInfo } from "hono/conninfo"; + +//#region src/conninfo.d.ts +/** + * ConnInfo Helper for Node.js + * @param c Context + * @returns ConnInfo + */ +declare const getConnInfo: GetConnInfo; +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs new file mode 100644 index 000000000..29ec46d0d --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs @@ -0,0 +1,20 @@ +//#region src/conninfo.ts +/** +* ConnInfo Helper for Node.js +* @param c Context +* @returns ConnInfo +*/ +const getConnInfo = (c) => { + const bindings = c.env.server ? c.env.server : c.env; + const address = bindings.incoming.socket.remoteAddress; + const port = bindings.incoming.socket.remotePort; + const family = bindings.incoming.socket.remoteFamily; + return { remote: { + address, + port, + addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0 + } }; +}; + +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs new file mode 100644 index 000000000..db7c31e48 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs @@ -0,0 +1,5 @@ +//#region src/utils/response/constants.ts +const X_ALREADY_SENT = "x-hono-already-sent"; + +//#endregion +export { X_ALREADY_SENT as t }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs new file mode 100644 index 000000000..8888a5804 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs @@ -0,0 +1,11 @@ + +//#region src/utils/response/constants.ts +const X_ALREADY_SENT = "x-hono-already-sent"; + +//#endregion +Object.defineProperty(exports, 'X_ALREADY_SENT', { + enumerable: true, + get: function () { + return X_ALREADY_SENT; + } +}); \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs new file mode 100644 index 000000000..aa3ceba6a --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs @@ -0,0 +1,35 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +//#region src/early-hints.ts +/** +* Early Hints middleware for Node.js +* Automatically sends a 103 Early Hints informational response with the specified Link header(s). +* +* @param options EarlyHintsOptions +* @returns MiddlewareHandler +*/ +const earlyHints = (options) => { + let warned = false; + return async (c, next) => { + const mode = c.req.header("Sec-Fetch-Mode"); + const dest = c.req.header("Sec-Fetch-Dest"); + if (mode && mode !== "navigate" || dest && dest !== "document") return next(); + const env = c.env || {}; + const outgoing = ("server" in env ? env.server : env)?.outgoing; + if (typeof outgoing?.writeEarlyHints !== "function") { + if (!warned) { + console.warn("Early Hints Middleware is not supported because writeEarlyHints is not defined."); + warned = true; + } + return await next(); + } + if (!outgoing.headersSent) { + const link = typeof options.link === "function" ? options.link(c) : options.link; + if (link !== void 0 && (Array.isArray(link) ? link.length > 0 : Boolean(link))) outgoing.writeEarlyHints({ link }); + } + await next(); + }; +}; + +//#endregion +exports.earlyHints = earlyHints; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts new file mode 100644 index 000000000..9bd27d5e3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts @@ -0,0 +1,16 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/early-hints.d.ts +type EarlyHintsOptions = { + link: string | string[] | ((c: Context) => string | string[] | undefined); +}; +/** + * Early Hints middleware for Node.js + * Automatically sends a 103 Early Hints informational response with the specified Link header(s). + * + * @param options EarlyHintsOptions + * @returns MiddlewareHandler + */ +declare const earlyHints: (options: EarlyHintsOptions) => MiddlewareHandler; +//#endregion +export { EarlyHintsOptions, earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts new file mode 100644 index 000000000..9bd27d5e3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts @@ -0,0 +1,16 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/early-hints.d.ts +type EarlyHintsOptions = { + link: string | string[] | ((c: Context) => string | string[] | undefined); +}; +/** + * Early Hints middleware for Node.js + * Automatically sends a 103 Early Hints informational response with the specified Link header(s). + * + * @param options EarlyHintsOptions + * @returns MiddlewareHandler + */ +declare const earlyHints: (options: EarlyHintsOptions) => MiddlewareHandler; +//#endregion +export { EarlyHintsOptions, earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs new file mode 100644 index 000000000..7999a3d0c --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs @@ -0,0 +1,33 @@ +//#region src/early-hints.ts +/** +* Early Hints middleware for Node.js +* Automatically sends a 103 Early Hints informational response with the specified Link header(s). +* +* @param options EarlyHintsOptions +* @returns MiddlewareHandler +*/ +const earlyHints = (options) => { + let warned = false; + return async (c, next) => { + const mode = c.req.header("Sec-Fetch-Mode"); + const dest = c.req.header("Sec-Fetch-Dest"); + if (mode && mode !== "navigate" || dest && dest !== "document") return next(); + const env = c.env || {}; + const outgoing = ("server" in env ? env.server : env)?.outgoing; + if (typeof outgoing?.writeEarlyHints !== "function") { + if (!warned) { + console.warn("Early Hints Middleware is not supported because writeEarlyHints is not defined."); + warned = true; + } + return await next(); + } + if (!outgoing.headersSent) { + const link = typeof options.link === "function" ? options.link(c) : options.link; + if (link !== void 0 && (Array.isArray(link) ? link.length > 0 : Boolean(link))) outgoing.writeEarlyHints({ link }); + } + await next(); + }; +}; + +//#endregion +export { earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs new file mode 100644 index 000000000..e578de054 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs @@ -0,0 +1,1340 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_constants = require('./constants-BXAKTxRC.cjs'); +let node_http = require("node:http"); +let node_http2 = require("node:http2"); +let node_stream = require("node:stream"); +let hono_ws = require("hono/ws"); + +//#region src/error.ts +var RequestError = class extends Error { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } +}; + +//#endregion +//#region src/headers.ts +const nonJoinedHeaders = new Set([ + "age", + "authorization", + "content-length", + "content-type", + "etag", + "expires", + "from", + "host", + "if-modified-since", + "if-unmodified-since", + "last-modified", + "location", + "max-forwards", + "proxy-authorization", + "referer", + "retry-after", + "server", + "user-agent" +]); +const validHeaderName = /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/; +const isHttpWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32; +const normalizeHeaderValue = (value) => { + if (!isHttpWhitespace(value.charCodeAt(0)) && !isHttpWhitespace(value.charCodeAt(value.length - 1))) return value; + let start = 0; + let end = value.length; + while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++; + while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--; + return value.slice(start, end); +}; +const forbiddenHeaderValue = /[\0\r\n]/; +const GlobalHeaders = globalThis.Headers; +const materializeHeaders = (rawHeaders, HeadersCtor = GlobalHeaders) => { + const headers = new HeadersCtor(); + for (let i = 0; i < rawHeaders.length; i += 2) { + const name = rawHeaders[i]; + if (!name.startsWith(":")) headers.append(name, rawHeaders[i + 1]); + } + return headers; +}; +var RequestHeaders = class { + #incoming; + #rawHeaders; + #headers; + #invalidValue; + constructor(incoming) { + this.#incoming = incoming; + if (incoming instanceof node_http2.Http2ServerRequest) this.#rawHeaders = incoming.rawHeaders.slice(); + } + get #lazyRawHeaders() { + return this.#rawHeaders ??= this.#incoming.rawHeaders.slice(); + } + get #native() { + if (!this.#headers) { + this.#headers = materializeHeaders(this.#lazyRawHeaders); + this.#rawHeaders = void 0; + } + return this.#headers; + } + #normalizedName(name) { + if (typeof name !== "string") return; + if (!validHeaderName.test(name)) throw new TypeError(`Invalid header name: ${name}`); + return name.toLowerCase(); + } + #lookupHttp1(lowerName) { + const headers = this.#incoming instanceof node_http2.Http2ServerRequest ? void 0 : this.#incoming.headers; + if (!headers || nonJoinedHeaders.has(lowerName) || lowerName === "set-cookie" || lowerName === "__proto__") return; + if (!Object.hasOwn(headers, lowerName)) return null; + const rawValue = headers[lowerName]; + if (typeof rawValue === "string") { + const value = normalizeHeaderValue(rawValue); + return forbiddenHeaderValue.test(value) ? void 0 : value; + } + } + #lookup(rawHeaders, lowerName) { + const separator = lowerName === "cookie" ? "; " : ", "; + let value = null; + for (let i = 0; i < rawHeaders.length; i += 2) { + const rawName = rawHeaders[i]; + if (rawName.length === lowerName.length && rawName.toLowerCase() === lowerName) { + const rawValue = normalizeHeaderValue(rawHeaders[i + 1]); + if (forbiddenHeaderValue.test(rawValue)) { + this.#invalidValue = true; + return; + } + value = value === null ? rawValue : value + separator + rawValue; + } + } + return value; + } + append(name, value) { + this.#native.append(name, value); + } + delete(name) { + this.#native.delete(name); + } + get(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value; + } + return this.#native.get(name); + } + has(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value !== null; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value !== null; + } + return this.#native.has(name); + } + set(name, value) { + this.#native.set(name, value); + } + getSetCookie() { + return this.#native.getSetCookie(); + } + keys() { + return this.#native.keys(); + } + values() { + return this.#native.values(); + } + entries() { + return this.#native.entries(); + } + forEach(callback, thisArg) { + this.#native.forEach((value, key) => { + callback.call(thisArg, value, key, this); + }); + } + [Symbol.iterator]() { + return this.entries(); + } +}; +Object.defineProperty(RequestHeaders.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Headers (lightweight) ${inspectFn(Object.fromEntries(this), { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(RequestHeaders.prototype, GlobalHeaders.prototype); +const newHeadersFromIncoming = (incoming) => globalThis.Headers === GlobalHeaders ? new RequestHeaders(incoming) : materializeHeaders(incoming.rawHeaders, globalThis.Headers); + +//#endregion +//#region src/url.ts +const reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; +const buildUrl = (scheme, host, incomingUrl) => { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); + return urlObj.href; + } else if (incomingUrl.length === 0) return url + "/"; + else { + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; + return url; + } +}; + +//#endregion +//#region src/request.ts +const toRequestError = (e) => { + if (e instanceof RequestError) return e; + return new RequestError(e.message, { cause: e }); +}; +const GlobalRequest = global.Request; +var Request$1 = class extends GlobalRequest { + constructor(input, options) { + if (typeof input === "object" && getRequestCache in input) { + const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; + if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); + input = input[getRequestCache](); + } + if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; + super(input, options); + } +}; +const wrapBodyStream = Symbol("wrapBodyStream"); +const byteExactEncodings = new Set([ + "latin1", + "binary", + "hex", + "base64", + "base64url" +]); +const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding); +const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect"); +const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect"); +const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8"); +const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof node_http2.Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding); +const recordBodyBufferedBeforeDisconnect = (incoming) => { + if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength; +}; +const readBodyBufferedBeforeDisconnect = (incoming, chunks) => { + if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]; + let result; + const errored = incoming.errored; + if (errored && errored.code !== "ECONNRESET") result = errored; + else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError(); + else { + const bodyChunks = chunks ?? []; + const chunk = incoming.read(); + if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks); + result = buffer; + const contentLength = incoming.headers["content-length"]; + if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) { + const expectedLength = Number(contentLength); + if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError(); + } + } + incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result; + return result; +}; +const enqueueBufferedBody = (controller, buffered) => { + if (buffered instanceof Error) { + controller.error(buffered); + return; + } + if (buffered.length > 0) controller.enqueue(buffered); + controller.close(); +}; +const newRequestFromIncoming = (method, url, headers, incoming, abortController) => { + const init = { + method, + headers, + signal: abortController.signal + }; + if (method === "TRACE") { + init.method = "GET"; + const req = new Request$1(url, init); + Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return req; + } + if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { + controller.enqueue(incoming.rawBody); + controller.close(); + } }); + else if (incoming[wrapBodyStream]) { + let reader; + init.body = new ReadableStream({ async pull(controller) { + try { + if (!reader) { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + enqueueBufferedBody(controller, buffered); + return; + } + } + reader ||= node_stream.Readable.toWeb(incoming).getReader(); + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + } catch (error) { + controller.error(error); + } + } }); + } else { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) init.body = new ReadableStream({ start(controller) { + enqueueBufferedBody(controller, buffered); + } }); + else init.body = node_stream.Readable.toWeb(incoming); + } + return new Request$1(url, init); +}; +const getRequestCache = Symbol("getRequestCache"); +const requestCache = Symbol("requestCache"); +const incomingKey = Symbol("incomingKey"); +const urlKey = Symbol("urlKey"); +const methodKey = Symbol("methodKey"); +const headersKey = Symbol("headersKey"); +const abortControllerKey = Symbol("abortControllerKey"); +const getAbortController = Symbol("getAbortController"); +const abortRequest = Symbol("abortRequest"); +const bodyBufferKey = Symbol("bodyBuffer"); +const bodyReadPromiseKey = Symbol("bodyReadPromise"); +const bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); +const bodyLockReaderKey = Symbol("bodyLockReader"); +const abortReasonKey = Symbol("abortReason"); +const newBodyUnusableError = () => { + return /* @__PURE__ */ new TypeError("Body is unusable"); +}; +const rejectBodyUnusable = () => { + return Promise.reject(newBodyUnusableError()); +}; +const textDecoder = new TextDecoder(); +const consumeBodyDirectOnce = (request) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + request[bodyConsumedDirectlyKey] = true; +}; +const toArrayBuffer = (buf) => { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +}; +const contentType = (request) => { + return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; +}; +const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; +const normalizeIncomingMethod = (method) => { + if (typeof method !== "string" || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": return upper; + default: return method; + } +}; +const validateDirectReadMethod = (method) => { + if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); + const normalized = method.toUpperCase(); + if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); +}; +const readBodyWithFastPath = (request, method, fromBuffer) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + const methodName = request.method; + if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); + const methodValidationError = validateDirectReadMethod(methodName); + if (methodValidationError) return Promise.reject(methodValidationError); + if (request[requestCache]) { + if (methodName !== "TRACE") return request[requestCache][method](); + } + const alreadyUsedError = consumeBodyDirectOnce(request); + if (alreadyUsedError) return alreadyUsedError; + const raw = readRawBodyIfAvailable(request); + if (raw) { + const result = Promise.resolve(fromBuffer(raw, request)); + request[bodyBufferKey] = void 0; + return result; + } + return readBodyDirect(request).then((buf) => { + const result = fromBuffer(buf, request); + request[bodyBufferKey] = void 0; + return result; + }); +}; +const readRawBodyIfAvailable = (request) => { + const incoming = request[incomingKey]; + if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; +}; +const normalizeAbortError = (request, incoming) => { + if (incoming.errored) return incoming.errored; + const reason = request[abortReasonKey]; + if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason)); + return /* @__PURE__ */ new Error("Client connection prematurely closed."); +}; +const readBodyDirect = (request) => { + if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); + if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; + const incoming = request[incomingKey]; + if (incoming.readableDidRead) return rejectBodyUnusable(); + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + if (buffered instanceof Error) return Promise.reject(buffered); + request[bodyBufferKey] = buffered; + return Promise.resolve(buffered); + } + const promise = new Promise((resolve, reject) => { + const chunks = []; + let settled = false; + const finish = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + const recoverCompleteBodyAfterDisconnect = (error) => { + const streamError = incoming.errored ?? error; + if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false; + finish(() => { + const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks); + if (recovered instanceof Error) reject(recovered); + else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming)); + else { + request[bodyBufferKey] = recovered; + resolve(recovered); + } + }); + return true; + }; + const onData = (chunk) => { + chunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + }; + const onEnd = () => { + finish(() => { + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); + request[bodyBufferKey] = buffer; + resolve(buffer); + }); + }; + const onError = (error) => { + if (recoverCompleteBodyAfterDisconnect(error)) return; + finish(() => { + reject(error); + }); + }; + const onClose = () => { + if (incoming.readableEnded) { + onEnd(); + return; + } + if (recoverCompleteBodyAfterDisconnect()) return; + finish(() => { + reject(normalizeAbortError(request, incoming)); + }); + }; + const cleanup = () => { + incoming.off("data", onData); + incoming.off("end", onEnd); + incoming.off("error", onError); + incoming.off("close", onClose); + request[bodyReadPromiseKey] = void 0; + }; + incoming.on("data", onData); + incoming.on("end", onEnd); + incoming.on("error", onError); + incoming.on("close", onClose); + queueMicrotask(() => { + if (settled) return; + if (incoming.readableEnded) onEnd(); + else if (incoming.errored) onError(incoming.errored); + else if (incoming.destroyed) onClose(); + }); + }); + request[bodyReadPromiseKey] = promise; + return promise; +}; +const requestPrototype = { + get method() { + return this[methodKey]; + }, + get url() { + return this[urlKey]; + }, + get headers() { + return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); + }, + [abortRequest](reason) { + if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; + const abortController = this[abortControllerKey]; + if (abortController && !abortController.signal.aborted) abortController.abort(reason); + }, + [getAbortController]() { + this[abortControllerKey] ||= new AbortController(); + if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); + return this[abortControllerKey]; + }, + [getRequestCache]() { + const abortController = this[getAbortController](); + if (this[requestCache]) return this[requestCache]; + const method = this.method; + if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { + this[bodyBufferKey] = void 0; + const init = { + method: method === "TRACE" ? "GET" : method, + headers: this.headers, + signal: abortController.signal + }; + if (method !== "TRACE") { + init.body = new ReadableStream({ start(c) { + c.close(); + } }); + init.duplex = "half"; + } + const req = new Request$1(this[urlKey], init); + if (method === "TRACE") Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return this[requestCache] = req; + } + return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); + }, + get body() { + if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; + const request = this[getRequestCache](); + if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); + return request.body; + }, + get bodyUsed() { + if (this[bodyConsumedDirectlyKey]) return true; + if (this[requestCache]) return this[requestCache].bodyUsed; + return false; + } +}; +Object.defineProperty(requestPrototype, "signal", { get() { + return this[getAbortController]().signal; +} }); +[ + "cache", + "credentials", + "destination", + "integrity", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "keepalive" +].forEach((k) => { + Object.defineProperty(requestPrototype, k, { get() { + return this[getRequestCache]()[k]; + } }); +}); +["clone", "formData"].forEach((k) => { + Object.defineProperty(requestPrototype, k, { value: function() { + if (this[bodyConsumedDirectlyKey]) { + if (k === "clone") throw newBodyUnusableError(); + return rejectBodyUnusable(); + } + return this[getRequestCache]()[k](); + } }); +}); +Object.defineProperty(requestPrototype, "text", { value: function() { + return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); +} }); +Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { + return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); +} }); +Object.defineProperty(requestPrototype, "blob", { value: function() { + return readBodyWithFastPath(this, "blob", (buf, request) => { + const type = contentType(request); + const init = type ? { headers: { "content-type": type } } : void 0; + return new Response(buf, init).blob(); + }); +} }); +Object.defineProperty(requestPrototype, "json", { value: function() { + if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + return this.text().then(JSON.parse); +} }); +Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Request (lightweight) ${inspectFn({ + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(requestPrototype, Request$1.prototype); +const newRequest = (incoming, defaultHostname) => { + const req = Object.create(requestPrototype); + req[incomingKey] = incoming; + req[methodKey] = normalizeIncomingMethod(incoming.method); + const incomingUrl = incoming.url || ""; + if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { + if (incoming instanceof node_http2.Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); + try { + req[urlKey] = new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + return req; + } + const host = (incoming instanceof node_http2.Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; + if (!host) throw new RequestError("Missing host header"); + let scheme; + if (incoming instanceof node_http2.Http2ServerRequest) { + scheme = incoming.scheme; + if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); + } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; + try { + req[urlKey] = buildUrl(scheme, host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + else throw new RequestError("Invalid URL", { cause: e }); + } + return req; +}; + +//#endregion +//#region src/response.ts +const defaultContentType = "text/plain; charset=UTF-8"; +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +const cacheKey = Symbol("cache"); +const GlobalResponse = global.Response; +var Response$1 = class Response$1 { + #body; + #init; + [getResponseCache]() { + const cache = this[cacheKey]; + const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; + delete this[cacheKey]; + return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { + status: this.#init?.status, + statusText: this.#init?.statusText, + headers: liveHeaders + } : this.#init); + } + constructor(body, init) { + let headers; + this.#body = body; + if (init instanceof GlobalResponse) { + const cachedGlobalResponse = init[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + this[getResponseCache](); + return; + } + this.#init = init instanceof Response$1 ? init.#init : init; + headers = new Headers(init.headers); + } else this.#init = init; + if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ + init?.status || 200, + body ?? null, + headers || init?.headers + ]; + } + get headers() { + const cache = this[cacheKey]; + if (cache) { + if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); + return cache[2]; + } + return this[getResponseCache]().headers; + } + get status() { + return this[cacheKey]?.[0] ?? this[getResponseCache]().status; + } + get ok() { + const status = this.status; + return status >= 200 && status < 300; + } +}; +[ + "body", + "bodyUsed", + "redirected", + "statusText", + "trailers", + "type", + "url" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { get() { + return this[getResponseCache]()[k]; + } }); +}); +[ + "arrayBuffer", + "blob", + "clone", + "formData", + "json", + "text" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { value: function() { + return this[getResponseCache]()[k](); + } }); +}); +Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Response (lightweight) ${inspectFn({ + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(Response$1, GlobalResponse); +Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); +const validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; +const parseRedirectUrl = (url) => { + if (url instanceof URL) return url.href; + if (validRedirectUrl.test(url)) return url; + return new URL(url).href; +}; +const validRedirectStatuses = new Set([ + 301, + 302, + 303, + 307, + 308 +]); +Object.defineProperty(Response$1, "redirect", { + value: function redirect(url, status = 302) { + if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); + return new Response$1(null, { + status, + headers: { location: parseRedirectUrl(url) } + }); + }, + writable: true, + configurable: true +}); +Object.defineProperty(Response$1, "json", { + value: function json(data, init) { + const body = JSON.stringify(data); + if (body === void 0) throw new TypeError("The data is not JSON serializable"); + const initHeaders = init?.headers; + let headers; + if (initHeaders) { + headers = new Headers(initHeaders); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + } else headers = { "content-type": "application/json" }; + return new Response$1(body, { + status: init?.status ?? 200, + statusText: init?.statusText, + headers + }); + }, + writable: true, + configurable: true +}); + +//#endregion +//#region src/utils.ts +async function readWithoutBlocking(readPromise) { + return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); +} +function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { + const cancel = (error) => { + reader.cancel(error).catch(() => {}); + }; + writable.on("close", cancel); + writable.on("error", cancel); + (currentReadPromise ?? reader.read()).then(flow, handleStreamError); + return reader.closed.finally(() => { + writable.off("close", cancel); + writable.off("error", cancel); + }); + function handleStreamError(error) { + if (error) writable.destroy(error); + } + function onDrain() { + reader.read().then(flow, handleStreamError); + } + function flow({ done, value }) { + try { + if (done) writable.end(); + else if (!writable.write(value)) writable.once("drain", onDrain); + else return reader.read().then(flow, handleStreamError); + } catch (e) { + handleStreamError(e); + } + } +} +function writeFromReadableStream(stream, writable) { + if (stream.locked) throw new TypeError("ReadableStream is locked."); + else if (writable.destroyed) return; + return writeFromReadableStreamDefaultReader(stream.getReader(), writable); +} +const buildOutgoingHttpHeaders = (headers, defaultContentType) => { + const res = {}; + if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); + if (headers.has("set-cookie")) { + const cookies = []; + for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); + else res[k] = v; + if (cookies.length > 0) res["set-cookie"] = cookies; + } else for (const [k, v] of headers) res[k] = v; + if (defaultContentType) res["content-type"] ??= defaultContentType; + return res; +}; + +//#endregion +//#region src/listener.ts +const outgoingEnded = Symbol("outgoingEnded"); +const incomingDraining = Symbol("incomingDraining"); +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; +const drainIncoming = (incoming) => { + const incomingWithDrainState = incoming; + if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; + incomingWithDrainState[incomingDraining] = true; + if (incoming instanceof node_http2.Http2ServerRequest) { + try { + incoming.stream?.close?.(node_http2.constants.NGHTTP2_NO_ERROR); + } catch {} + return; + } + let bytesRead = 0; + const cleanup = () => { + clearTimeout(timer); + incoming.off("data", onData); + incoming.off("end", cleanup); + incoming.off("error", cleanup); + }; + const forceClose = () => { + cleanup(); + const socket = incoming.socket; + if (socket && !socket.destroyed) { + if (typeof socket.destroySoon === "function") socket.destroySoon(); + else if (typeof socket.destroy === "function") socket.destroy(); + } + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref?.(); + const onData = (chunk) => { + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }; + incoming.on("data", onData); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +}; +const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { + if (incoming.errored) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest](incoming.errored.toString()); + } else if (!outgoing.writableFinished || incoming instanceof node_http2.Http2ServerRequest && incoming.aborted) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest]("Client connection prematurely closed."); + } + if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + drainIncoming(incoming); + }); + }); +}; +const isImmediateCacheableResponse = (res) => { + if (!(cacheKey in res)) return false; + const body = res[cacheKey][1]; + return body === null || typeof body === "string" || body instanceof Uint8Array; +}; +const handleRequestError = () => new Response(null, { status: 400 }); +const handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); +const handleResponseError = (e, outgoing) => { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); + else { + console.error(e); + if (!outgoing.headersSent) { + if (outgoing instanceof node_http.ServerResponse) outgoing._contentLength = null; + outgoing.writeHead(500, { "Content-Type": "text/plain" }); + } + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(err); + } +}; +const flushHeaders = (outgoing) => { + if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); +}; +const trySetContentLength = (outgoing, status, length) => { + const http1 = outgoing; + if (http1._contentLength === null && http1._hasBody && http1.useChunkedEncodingByDefault && !http1._removedContLen && status >= 200 && status !== 204 && status !== 304 && !outgoing.hasHeader("content-length") && !outgoing.hasHeader("transfer-encoding") && !outgoing.hasHeader("trailer")) { + http1._contentLength = length; + return true; + } + return false; +}; +const writeDefaultHeaders = (outgoing, status, length) => { + if (trySetContentLength(outgoing, status, length)) outgoing.writeHead(status, { "Content-Type": defaultContentType }); + else outgoing.writeHead(status, { + "Content-Type": defaultContentType, + "Content-Length": length + }); +}; +const responseViaCache = async (res, outgoing) => { + let [status, body, header] = res[cacheKey]; + if (!header) { + if (body === null) { + outgoing.writeHead(status); + outgoing.end(); + } else if (typeof body === "string") { + writeDefaultHeaders(outgoing, status, Buffer.byteLength(body)); + outgoing.end(body); + } else if (body instanceof Uint8Array) { + writeDefaultHeaders(outgoing, status, body.byteLength); + outgoing.end(body); + } else if (body instanceof Blob) { + writeDefaultHeaders(outgoing, status, body.size); + outgoing.end(new Uint8Array(await body.arrayBuffer())); + } else { + outgoing.writeHead(status, { "Content-Type": defaultContentType }); + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); + return; + } + let hasContentLength = false; + let plainHeaders = false; + let canAutoLength = true; + if (header instanceof Headers) { + hasContentLength = header.has("content-length"); + header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); + } else if (Array.isArray(header)) { + const headerObj = new Headers(header); + hasContentLength = headerObj.has("content-length"); + header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); + } else { + plainHeaders = true; + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === "content-length") { + hasContentLength = true; + break; + } + if (key.length === 17 && key.toLowerCase() === "transfer-encoding" || key.length === 7 && key.toLowerCase() === "trailer") canAutoLength = false; + } + } + if (!hasContentLength) { + let length; + if (typeof body === "string") length = Buffer.byteLength(body); + else if (body instanceof Uint8Array) length = body.byteLength; + else if (body instanceof Blob) length = body.size; + if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) { + if (plainHeaders) header = { ...header }; + header["Content-Length"] = length; + } + } + outgoing.writeHead(status, header); + if (body == null) outgoing.end(); + else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); + else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); + else { + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); +}; +const isPromise = (res) => typeof res.then === "function"; +const responseViaResponseObject = async (res, outgoing, options = {}) => { + if (isPromise(res)) if (options.errorHandler) try { + res = await res; + } catch (err) { + const errRes = await options.errorHandler(err); + if (!errRes) return; + res = errRes; + } + else res = await res.catch(handleFetchError); + if (cacheKey in res) return responseViaCache(res, outgoing); + const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); + if (res.body) { + const reader = res.body.getReader(); + const values = []; + let done = false; + let currentReadPromise = void 0; + if (resHeaderRecord["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + currentReadPromise ||= reader.read(); + const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { + console.error(e); + done = true; + }); + if (!chunk) { + if (i === 1) { + await new Promise((resolve) => setTimeout(resolve)); + maxReadCount = 3; + continue; + } + break; + } + currentReadPromise = void 0; + if (chunk.value) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); + } + outgoing.writeHead(res.status, resHeaderRecord); + values.forEach((value) => { + outgoing.write(value); + }); + if (done) outgoing.end(); + else { + if (values.length === 0) flushHeaders(outgoing); + await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); + } + } else if (resHeaderRecord[require_constants.X_ALREADY_SENT]) {} else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } + outgoing[outgoingEnded]?.(); +}; +const getRequestListener = (fetchCallback, options = {}) => { + const autoCleanupIncoming = options.autoCleanupIncoming ?? true; + if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { + Object.defineProperty(global, "Request", { value: Request$1 }); + Object.defineProperty(global, "Response", { value: Response$1 }); + } + return async (incoming, outgoing) => { + let res, req; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = () => { + if (!req || closeHandlerAttached) return; + closeHandlerAttached = true; + outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); + }; + try { + req = newRequest(incoming, options.hostname); + needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) { + incoming[wrapBodyStream] = true; + if (incoming instanceof node_http2.Http2ServerRequest) outgoing[outgoingEnded] = () => { + if (!incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + incoming.destroy(); + outgoing.destroy(); + }); + }); + }; + } + res = fetchCallback(req, { + incoming, + outgoing + }); + if (!isPromise(res) && isImmediateCacheableResponse(res)) { + if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { + if (!incoming.readableEnded) drainIncoming(incoming); + }); + return responseViaCache(res, outgoing); + } + ensureCloseHandler(); + } catch (e) { + if (!res) if (options.errorHandler) { + ensureCloseHandler(); + res = await options.errorHandler(req ? e : toRequestError(e)); + if (!res) return; + } else if (!req) res = handleRequestError(); + else res = handleFetchError(e); + else return handleResponseError(e, outgoing); + } + try { + return await responseViaResponseObject(res, outgoing, options); + } catch (e) { + return handleResponseError(e, outgoing); + } + }; +}; + +//#endregion +//#region src/websocket.ts +/** +* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +const CloseEvent = globalThis.CloseEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get wasClean() { + return this.#eventInitDict.wasClean ?? false; + } + get code() { + return this.#eventInitDict.code ?? 0; + } + get reason() { + return this.#eventInitDict.reason ?? ""; + } +}; +/** +* Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`. +* @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent +*/ +const ErrorEvent = globalThis.ErrorEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get message() { + return this.#eventInitDict.message ?? ""; + } + get filename() { + return this.#eventInitDict.filename ?? ""; + } + get lineno() { + return this.#eventInitDict.lineno ?? 0; + } + get colno() { + return this.#eventInitDict.colno ?? 0; + } + get error() { + return this.#eventInitDict.error ?? null; + } +}; +const generateConnectionSymbol = () => Symbol("connection"); +const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); +const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); +const responseHeadersToSkip = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-protocol" +]); +const appendResponseHeaders = (headers, responseHeaders) => { + if (!responseHeaders) return; + responseHeaders.forEach((value, key) => { + if (responseHeadersToSkip.has(key.toLowerCase())) return; + headers.push(`${key}: ${value}`); + }); +}; +const rejectUpgradeRequest = (socket, status, responseHeaders) => { + const responseLines = ["Connection: close", "Content-Length: 0"]; + appendResponseHeaders(responseLines, responseHeaders); + socket.end(`HTTP/1.1 ${status.toString()} ${node_http.STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r +`); +}; +const createUpgradeRequest = (request) => { + const protocol = request.socket.encrypted ? "https" : "http"; + const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); + const headers = new Headers(); + for (const key in request.headers) { + const value = request.headers[key]; + if (!value) continue; + headers.append(key, Array.isArray(value) ? value[0] : value); + } + return new Request(url, { headers }); +}; +const setupWebSocket = (options) => { + const { server, fetchCallback, wss } = options; + const waiterMap = /* @__PURE__ */ new Map(); + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiter.resolve(ws); + waiterMap.delete(request); + } + }); + const rejectWaiter = (request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiterMap.delete(request); + waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted")); + } + }; + const waitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve, reject) => { + waiterMap.set(request, { + resolve, + reject, + connectionSymbol + }); + }); + }; + server.on("upgrade", async (request, socket, head) => { + if (request.headers.upgrade?.toLowerCase() !== "websocket") return; + const env = { + incoming: request, + outgoing: void 0, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket + }; + let status = 400; + let responseHeaders; + try { + const response = await fetchCallback(createUpgradeRequest(request), env); + if (response instanceof Response) { + status = response.status; + responseHeaders = response.headers; + } + } catch { + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); + return; + } + const waiter = waiterMap.get(request); + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + rejectWaiter(request); + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); + return; + } + const addResponseHeaders = (headers) => { + appendResponseHeaders(headers, responseHeaders); + }; + const reclaimWaiterOnClose = () => rejectWaiter(request); + socket.once("close", reclaimWaiterOnClose); + wss.on("headers", addResponseHeaders); + try { + wss.handleUpgrade(request, socket, head, (ws) => { + socket.off("close", reclaimWaiterOnClose); + wss.emit("connection", ws, request); + }); + } finally { + wss.off("headers", addResponseHeaders); + } + }); + server.on("close", () => { + wss.close(); + }); +}; +const upgradeWebSocket = (0, hono_ws.defineWebSocketHelper)(async (c, events, options) => { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; + const env = c.env; + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; + if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); + const connectionSymbol = generateConnectionSymbol(); + env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + let ws; + try { + ws = await waitForWebSocket(env.incoming, connectionSymbol); + } catch { + return; + } + const messagesReceivedInStarting = []; + const bufferMessage = (data, isBinary) => { + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); + const ctx = { + binaryType: "arraybuffer", + close(code, reason) { + ws.close(code, reason); + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState; + }, + send(source, opts) { + ws.send(source, { compress: opts?.compress }); + }, + url: new URL(c.req.url) + }; + try { + events?.onOpen?.(new Event("open"), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + const handleMessage = (data, isBinary) => { + const datas = Array.isArray(data) ? data : [data]; + for (const data of datas) try { + events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }; + ws.off("message", bufferMessage); + for (const message of messagesReceivedInStarting) handleMessage(...message); + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { + try { + events?.onClose?.(new CloseEvent("close", { + code, + reason: reason.toString() + }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + ws.on("error", (error) => { + try { + events?.onError?.(new ErrorEvent("error", { error }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + })(); + return new Response(); +}); + +//#endregion +//#region src/server.ts +const createAdaptorServer = (options) => { + const fetchCallback = options.fetch; + const requestListener = getRequestListener(fetchCallback, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming + }); + const server = (options.createServer || node_http.createServer)(options.serverOptions || {}, requestListener); + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); + setupWebSocket({ + server, + fetchCallback, + wss: options.websocket.server + }); + } + return server; +}; +const serve = (options, listeningListener) => { + const server = createAdaptorServer(options); + server.listen(options?.port ?? 3e3, options.hostname, () => { + const serverInfo = server.address(); + listeningListener && listeningListener(serverInfo); + }); + return server; +}; + +//#endregion +exports.RequestError = RequestError; +exports.createAdaptorServer = createAdaptorServer; +exports.getRequestListener = getRequestListener; +exports.serve = serve; +exports.upgradeWebSocket = upgradeWebSocket; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts new file mode 100644 index 000000000..d3c55ce99 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts @@ -0,0 +1,101 @@ +import { AddressInfo } from "node:net"; +import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http"; +import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2"; +import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https"; +import { Duplex } from "node:stream"; +import { UpgradeWebSocket } from "hono/ws"; + +//#region src/websocket-types.d.ts +type WSReadyState = 0 | 1 | 2 | 3; +type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[]; +type WebSocketSendOptions = { + compress?: boolean; +}; +interface WebSocketLike { + protocol: string; + readyState: WSReadyState; + close(code?: number, reason?: string): void; + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void; + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this; + on(event: 'error', listener: (error: unknown) => void): this; + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; +} +interface WebSocketServerLike { + options: { + noServer?: boolean; + }; + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this; + on(event: 'headers', listener: (headers: string[]) => void): this; + off(event: 'headers', listener: (headers: string[]) => void): this; + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean; + handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void; + close(): void; +} +//#endregion +//#region src/types.d.ts +type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; +type Http2Bindings = { + incoming: Http2ServerRequest; + outgoing: Http2ServerResponse; +}; +type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise | unknown; +type ServerType = Server | Http2Server | Http2SecureServer; +type createHttpOptions = { + serverOptions?: ServerOptions; + createServer?: typeof createServer; +}; +type createHttpsOptions = { + serverOptions?: ServerOptions$2; + createServer?: typeof createServer$2; +}; +type createHttp2Options = { + serverOptions?: ServerOptions$1; + createServer?: typeof createServer$1; +}; +type createSecureHttp2Options = { + serverOptions?: SecureServerOptions; + createServer?: typeof createSecureServer; +}; +type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options; +type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; + websocket?: { + server: WebSocketServerLike; + }; +} & ServerOptions$3; +type CustomErrorHandler = (err: unknown) => void | Response | Promise; +//#endregion +//#region src/server.d.ts +declare const createAdaptorServer: (options: Options) => ServerType; +declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType; +//#endregion +//#region src/websocket.d.ts +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void; +}; +declare const upgradeWebSocket: UpgradeWebSocket; +//#endregion +//#region src/listener.d.ts +declare const getRequestListener: (fetchCallback: FetchCallback, options?: { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise; +//#endregion +//#region src/error.d.ts +declare class RequestError extends Error { + constructor(message: string, options?: { + cause?: unknown; + }); +} +//#endregion +export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts new file mode 100644 index 000000000..a815b86f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts @@ -0,0 +1,101 @@ +import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http"; +import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2"; +import { Duplex } from "node:stream"; +import { UpgradeWebSocket } from "hono/ws"; +import { AddressInfo } from "node:net"; +import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https"; + +//#region src/websocket-types.d.ts +type WSReadyState = 0 | 1 | 2 | 3; +type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[]; +type WebSocketSendOptions = { + compress?: boolean; +}; +interface WebSocketLike { + protocol: string; + readyState: WSReadyState; + close(code?: number, reason?: string): void; + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void; + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this; + on(event: 'error', listener: (error: unknown) => void): this; + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; +} +interface WebSocketServerLike { + options: { + noServer?: boolean; + }; + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this; + on(event: 'headers', listener: (headers: string[]) => void): this; + off(event: 'headers', listener: (headers: string[]) => void): this; + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean; + handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void; + close(): void; +} +//#endregion +//#region src/types.d.ts +type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; +type Http2Bindings = { + incoming: Http2ServerRequest; + outgoing: Http2ServerResponse; +}; +type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise | unknown; +type ServerType = Server | Http2Server | Http2SecureServer; +type createHttpOptions = { + serverOptions?: ServerOptions; + createServer?: typeof createServer; +}; +type createHttpsOptions = { + serverOptions?: ServerOptions$2; + createServer?: typeof createServer$2; +}; +type createHttp2Options = { + serverOptions?: ServerOptions$1; + createServer?: typeof createServer$1; +}; +type createSecureHttp2Options = { + serverOptions?: SecureServerOptions; + createServer?: typeof createSecureServer; +}; +type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options; +type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; + websocket?: { + server: WebSocketServerLike; + }; +} & ServerOptions$3; +type CustomErrorHandler = (err: unknown) => void | Response | Promise; +//#endregion +//#region src/server.d.ts +declare const createAdaptorServer: (options: Options) => ServerType; +declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType; +//#endregion +//#region src/websocket.d.ts +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void; +}; +declare const upgradeWebSocket: UpgradeWebSocket; +//#endregion +//#region src/listener.d.ts +declare const getRequestListener: (fetchCallback: FetchCallback, options?: { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise; +//#endregion +//#region src/error.d.ts +declare class RequestError extends Error { + constructor(message: string, options?: { + cause?: unknown; + }); +} +//#endregion +export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs new file mode 100644 index 000000000..4f5954d70 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs @@ -0,0 +1,1335 @@ +import { t as X_ALREADY_SENT } from "./constants-BLSFu_RU.mjs"; +import { STATUS_CODES, ServerResponse, createServer } from "node:http"; +import { Http2ServerRequest, constants } from "node:http2"; +import { Readable } from "node:stream"; +import { defineWebSocketHelper } from "hono/ws"; + +//#region src/error.ts +var RequestError = class extends Error { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } +}; + +//#endregion +//#region src/headers.ts +const nonJoinedHeaders = new Set([ + "age", + "authorization", + "content-length", + "content-type", + "etag", + "expires", + "from", + "host", + "if-modified-since", + "if-unmodified-since", + "last-modified", + "location", + "max-forwards", + "proxy-authorization", + "referer", + "retry-after", + "server", + "user-agent" +]); +const validHeaderName = /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/; +const isHttpWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32; +const normalizeHeaderValue = (value) => { + if (!isHttpWhitespace(value.charCodeAt(0)) && !isHttpWhitespace(value.charCodeAt(value.length - 1))) return value; + let start = 0; + let end = value.length; + while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++; + while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--; + return value.slice(start, end); +}; +const forbiddenHeaderValue = /[\0\r\n]/; +const GlobalHeaders = globalThis.Headers; +const materializeHeaders = (rawHeaders, HeadersCtor = GlobalHeaders) => { + const headers = new HeadersCtor(); + for (let i = 0; i < rawHeaders.length; i += 2) { + const name = rawHeaders[i]; + if (!name.startsWith(":")) headers.append(name, rawHeaders[i + 1]); + } + return headers; +}; +var RequestHeaders = class { + #incoming; + #rawHeaders; + #headers; + #invalidValue; + constructor(incoming) { + this.#incoming = incoming; + if (incoming instanceof Http2ServerRequest) this.#rawHeaders = incoming.rawHeaders.slice(); + } + get #lazyRawHeaders() { + return this.#rawHeaders ??= this.#incoming.rawHeaders.slice(); + } + get #native() { + if (!this.#headers) { + this.#headers = materializeHeaders(this.#lazyRawHeaders); + this.#rawHeaders = void 0; + } + return this.#headers; + } + #normalizedName(name) { + if (typeof name !== "string") return; + if (!validHeaderName.test(name)) throw new TypeError(`Invalid header name: ${name}`); + return name.toLowerCase(); + } + #lookupHttp1(lowerName) { + const headers = this.#incoming instanceof Http2ServerRequest ? void 0 : this.#incoming.headers; + if (!headers || nonJoinedHeaders.has(lowerName) || lowerName === "set-cookie" || lowerName === "__proto__") return; + if (!Object.hasOwn(headers, lowerName)) return null; + const rawValue = headers[lowerName]; + if (typeof rawValue === "string") { + const value = normalizeHeaderValue(rawValue); + return forbiddenHeaderValue.test(value) ? void 0 : value; + } + } + #lookup(rawHeaders, lowerName) { + const separator = lowerName === "cookie" ? "; " : ", "; + let value = null; + for (let i = 0; i < rawHeaders.length; i += 2) { + const rawName = rawHeaders[i]; + if (rawName.length === lowerName.length && rawName.toLowerCase() === lowerName) { + const rawValue = normalizeHeaderValue(rawHeaders[i + 1]); + if (forbiddenHeaderValue.test(rawValue)) { + this.#invalidValue = true; + return; + } + value = value === null ? rawValue : value + separator + rawValue; + } + } + return value; + } + append(name, value) { + this.#native.append(name, value); + } + delete(name) { + this.#native.delete(name); + } + get(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value; + } + return this.#native.get(name); + } + has(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value !== null; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value !== null; + } + return this.#native.has(name); + } + set(name, value) { + this.#native.set(name, value); + } + getSetCookie() { + return this.#native.getSetCookie(); + } + keys() { + return this.#native.keys(); + } + values() { + return this.#native.values(); + } + entries() { + return this.#native.entries(); + } + forEach(callback, thisArg) { + this.#native.forEach((value, key) => { + callback.call(thisArg, value, key, this); + }); + } + [Symbol.iterator]() { + return this.entries(); + } +}; +Object.defineProperty(RequestHeaders.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Headers (lightweight) ${inspectFn(Object.fromEntries(this), { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(RequestHeaders.prototype, GlobalHeaders.prototype); +const newHeadersFromIncoming = (incoming) => globalThis.Headers === GlobalHeaders ? new RequestHeaders(incoming) : materializeHeaders(incoming.rawHeaders, globalThis.Headers); + +//#endregion +//#region src/url.ts +const reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; +const buildUrl = (scheme, host, incomingUrl) => { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); + return urlObj.href; + } else if (incomingUrl.length === 0) return url + "/"; + else { + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; + return url; + } +}; + +//#endregion +//#region src/request.ts +const toRequestError = (e) => { + if (e instanceof RequestError) return e; + return new RequestError(e.message, { cause: e }); +}; +const GlobalRequest = global.Request; +var Request$1 = class extends GlobalRequest { + constructor(input, options) { + if (typeof input === "object" && getRequestCache in input) { + const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; + if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); + input = input[getRequestCache](); + } + if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; + super(input, options); + } +}; +const wrapBodyStream = Symbol("wrapBodyStream"); +const byteExactEncodings = new Set([ + "latin1", + "binary", + "hex", + "base64", + "base64url" +]); +const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding); +const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect"); +const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect"); +const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8"); +const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding); +const recordBodyBufferedBeforeDisconnect = (incoming) => { + if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength; +}; +const readBodyBufferedBeforeDisconnect = (incoming, chunks) => { + if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]; + let result; + const errored = incoming.errored; + if (errored && errored.code !== "ECONNRESET") result = errored; + else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError(); + else { + const bodyChunks = chunks ?? []; + const chunk = incoming.read(); + if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks); + result = buffer; + const contentLength = incoming.headers["content-length"]; + if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) { + const expectedLength = Number(contentLength); + if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError(); + } + } + incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result; + return result; +}; +const enqueueBufferedBody = (controller, buffered) => { + if (buffered instanceof Error) { + controller.error(buffered); + return; + } + if (buffered.length > 0) controller.enqueue(buffered); + controller.close(); +}; +const newRequestFromIncoming = (method, url, headers, incoming, abortController) => { + const init = { + method, + headers, + signal: abortController.signal + }; + if (method === "TRACE") { + init.method = "GET"; + const req = new Request$1(url, init); + Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return req; + } + if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { + controller.enqueue(incoming.rawBody); + controller.close(); + } }); + else if (incoming[wrapBodyStream]) { + let reader; + init.body = new ReadableStream({ async pull(controller) { + try { + if (!reader) { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + enqueueBufferedBody(controller, buffered); + return; + } + } + reader ||= Readable.toWeb(incoming).getReader(); + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + } catch (error) { + controller.error(error); + } + } }); + } else { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) init.body = new ReadableStream({ start(controller) { + enqueueBufferedBody(controller, buffered); + } }); + else init.body = Readable.toWeb(incoming); + } + return new Request$1(url, init); +}; +const getRequestCache = Symbol("getRequestCache"); +const requestCache = Symbol("requestCache"); +const incomingKey = Symbol("incomingKey"); +const urlKey = Symbol("urlKey"); +const methodKey = Symbol("methodKey"); +const headersKey = Symbol("headersKey"); +const abortControllerKey = Symbol("abortControllerKey"); +const getAbortController = Symbol("getAbortController"); +const abortRequest = Symbol("abortRequest"); +const bodyBufferKey = Symbol("bodyBuffer"); +const bodyReadPromiseKey = Symbol("bodyReadPromise"); +const bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); +const bodyLockReaderKey = Symbol("bodyLockReader"); +const abortReasonKey = Symbol("abortReason"); +const newBodyUnusableError = () => { + return /* @__PURE__ */ new TypeError("Body is unusable"); +}; +const rejectBodyUnusable = () => { + return Promise.reject(newBodyUnusableError()); +}; +const textDecoder = new TextDecoder(); +const consumeBodyDirectOnce = (request) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + request[bodyConsumedDirectlyKey] = true; +}; +const toArrayBuffer = (buf) => { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +}; +const contentType = (request) => { + return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; +}; +const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; +const normalizeIncomingMethod = (method) => { + if (typeof method !== "string" || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": return upper; + default: return method; + } +}; +const validateDirectReadMethod = (method) => { + if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); + const normalized = method.toUpperCase(); + if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); +}; +const readBodyWithFastPath = (request, method, fromBuffer) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + const methodName = request.method; + if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); + const methodValidationError = validateDirectReadMethod(methodName); + if (methodValidationError) return Promise.reject(methodValidationError); + if (request[requestCache]) { + if (methodName !== "TRACE") return request[requestCache][method](); + } + const alreadyUsedError = consumeBodyDirectOnce(request); + if (alreadyUsedError) return alreadyUsedError; + const raw = readRawBodyIfAvailable(request); + if (raw) { + const result = Promise.resolve(fromBuffer(raw, request)); + request[bodyBufferKey] = void 0; + return result; + } + return readBodyDirect(request).then((buf) => { + const result = fromBuffer(buf, request); + request[bodyBufferKey] = void 0; + return result; + }); +}; +const readRawBodyIfAvailable = (request) => { + const incoming = request[incomingKey]; + if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; +}; +const normalizeAbortError = (request, incoming) => { + if (incoming.errored) return incoming.errored; + const reason = request[abortReasonKey]; + if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason)); + return /* @__PURE__ */ new Error("Client connection prematurely closed."); +}; +const readBodyDirect = (request) => { + if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); + if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; + const incoming = request[incomingKey]; + if (incoming.readableDidRead) return rejectBodyUnusable(); + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + if (buffered instanceof Error) return Promise.reject(buffered); + request[bodyBufferKey] = buffered; + return Promise.resolve(buffered); + } + const promise = new Promise((resolve, reject) => { + const chunks = []; + let settled = false; + const finish = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + const recoverCompleteBodyAfterDisconnect = (error) => { + const streamError = incoming.errored ?? error; + if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false; + finish(() => { + const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks); + if (recovered instanceof Error) reject(recovered); + else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming)); + else { + request[bodyBufferKey] = recovered; + resolve(recovered); + } + }); + return true; + }; + const onData = (chunk) => { + chunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + }; + const onEnd = () => { + finish(() => { + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); + request[bodyBufferKey] = buffer; + resolve(buffer); + }); + }; + const onError = (error) => { + if (recoverCompleteBodyAfterDisconnect(error)) return; + finish(() => { + reject(error); + }); + }; + const onClose = () => { + if (incoming.readableEnded) { + onEnd(); + return; + } + if (recoverCompleteBodyAfterDisconnect()) return; + finish(() => { + reject(normalizeAbortError(request, incoming)); + }); + }; + const cleanup = () => { + incoming.off("data", onData); + incoming.off("end", onEnd); + incoming.off("error", onError); + incoming.off("close", onClose); + request[bodyReadPromiseKey] = void 0; + }; + incoming.on("data", onData); + incoming.on("end", onEnd); + incoming.on("error", onError); + incoming.on("close", onClose); + queueMicrotask(() => { + if (settled) return; + if (incoming.readableEnded) onEnd(); + else if (incoming.errored) onError(incoming.errored); + else if (incoming.destroyed) onClose(); + }); + }); + request[bodyReadPromiseKey] = promise; + return promise; +}; +const requestPrototype = { + get method() { + return this[methodKey]; + }, + get url() { + return this[urlKey]; + }, + get headers() { + return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); + }, + [abortRequest](reason) { + if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; + const abortController = this[abortControllerKey]; + if (abortController && !abortController.signal.aborted) abortController.abort(reason); + }, + [getAbortController]() { + this[abortControllerKey] ||= new AbortController(); + if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); + return this[abortControllerKey]; + }, + [getRequestCache]() { + const abortController = this[getAbortController](); + if (this[requestCache]) return this[requestCache]; + const method = this.method; + if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { + this[bodyBufferKey] = void 0; + const init = { + method: method === "TRACE" ? "GET" : method, + headers: this.headers, + signal: abortController.signal + }; + if (method !== "TRACE") { + init.body = new ReadableStream({ start(c) { + c.close(); + } }); + init.duplex = "half"; + } + const req = new Request$1(this[urlKey], init); + if (method === "TRACE") Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return this[requestCache] = req; + } + return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); + }, + get body() { + if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; + const request = this[getRequestCache](); + if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); + return request.body; + }, + get bodyUsed() { + if (this[bodyConsumedDirectlyKey]) return true; + if (this[requestCache]) return this[requestCache].bodyUsed; + return false; + } +}; +Object.defineProperty(requestPrototype, "signal", { get() { + return this[getAbortController]().signal; +} }); +[ + "cache", + "credentials", + "destination", + "integrity", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "keepalive" +].forEach((k) => { + Object.defineProperty(requestPrototype, k, { get() { + return this[getRequestCache]()[k]; + } }); +}); +["clone", "formData"].forEach((k) => { + Object.defineProperty(requestPrototype, k, { value: function() { + if (this[bodyConsumedDirectlyKey]) { + if (k === "clone") throw newBodyUnusableError(); + return rejectBodyUnusable(); + } + return this[getRequestCache]()[k](); + } }); +}); +Object.defineProperty(requestPrototype, "text", { value: function() { + return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); +} }); +Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { + return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); +} }); +Object.defineProperty(requestPrototype, "blob", { value: function() { + return readBodyWithFastPath(this, "blob", (buf, request) => { + const type = contentType(request); + const init = type ? { headers: { "content-type": type } } : void 0; + return new Response(buf, init).blob(); + }); +} }); +Object.defineProperty(requestPrototype, "json", { value: function() { + if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + return this.text().then(JSON.parse); +} }); +Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Request (lightweight) ${inspectFn({ + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(requestPrototype, Request$1.prototype); +const newRequest = (incoming, defaultHostname) => { + const req = Object.create(requestPrototype); + req[incomingKey] = incoming; + req[methodKey] = normalizeIncomingMethod(incoming.method); + const incomingUrl = incoming.url || ""; + if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { + if (incoming instanceof Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); + try { + req[urlKey] = new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + return req; + } + const host = (incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; + if (!host) throw new RequestError("Missing host header"); + let scheme; + if (incoming instanceof Http2ServerRequest) { + scheme = incoming.scheme; + if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); + } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; + try { + req[urlKey] = buildUrl(scheme, host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + else throw new RequestError("Invalid URL", { cause: e }); + } + return req; +}; + +//#endregion +//#region src/response.ts +const defaultContentType = "text/plain; charset=UTF-8"; +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +const cacheKey = Symbol("cache"); +const GlobalResponse = global.Response; +var Response$1 = class Response$1 { + #body; + #init; + [getResponseCache]() { + const cache = this[cacheKey]; + const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; + delete this[cacheKey]; + return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { + status: this.#init?.status, + statusText: this.#init?.statusText, + headers: liveHeaders + } : this.#init); + } + constructor(body, init) { + let headers; + this.#body = body; + if (init instanceof GlobalResponse) { + const cachedGlobalResponse = init[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + this[getResponseCache](); + return; + } + this.#init = init instanceof Response$1 ? init.#init : init; + headers = new Headers(init.headers); + } else this.#init = init; + if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ + init?.status || 200, + body ?? null, + headers || init?.headers + ]; + } + get headers() { + const cache = this[cacheKey]; + if (cache) { + if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); + return cache[2]; + } + return this[getResponseCache]().headers; + } + get status() { + return this[cacheKey]?.[0] ?? this[getResponseCache]().status; + } + get ok() { + const status = this.status; + return status >= 200 && status < 300; + } +}; +[ + "body", + "bodyUsed", + "redirected", + "statusText", + "trailers", + "type", + "url" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { get() { + return this[getResponseCache]()[k]; + } }); +}); +[ + "arrayBuffer", + "blob", + "clone", + "formData", + "json", + "text" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { value: function() { + return this[getResponseCache]()[k](); + } }); +}); +Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Response (lightweight) ${inspectFn({ + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(Response$1, GlobalResponse); +Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); +const validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; +const parseRedirectUrl = (url) => { + if (url instanceof URL) return url.href; + if (validRedirectUrl.test(url)) return url; + return new URL(url).href; +}; +const validRedirectStatuses = new Set([ + 301, + 302, + 303, + 307, + 308 +]); +Object.defineProperty(Response$1, "redirect", { + value: function redirect(url, status = 302) { + if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); + return new Response$1(null, { + status, + headers: { location: parseRedirectUrl(url) } + }); + }, + writable: true, + configurable: true +}); +Object.defineProperty(Response$1, "json", { + value: function json(data, init) { + const body = JSON.stringify(data); + if (body === void 0) throw new TypeError("The data is not JSON serializable"); + const initHeaders = init?.headers; + let headers; + if (initHeaders) { + headers = new Headers(initHeaders); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + } else headers = { "content-type": "application/json" }; + return new Response$1(body, { + status: init?.status ?? 200, + statusText: init?.statusText, + headers + }); + }, + writable: true, + configurable: true +}); + +//#endregion +//#region src/utils.ts +async function readWithoutBlocking(readPromise) { + return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); +} +function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { + const cancel = (error) => { + reader.cancel(error).catch(() => {}); + }; + writable.on("close", cancel); + writable.on("error", cancel); + (currentReadPromise ?? reader.read()).then(flow, handleStreamError); + return reader.closed.finally(() => { + writable.off("close", cancel); + writable.off("error", cancel); + }); + function handleStreamError(error) { + if (error) writable.destroy(error); + } + function onDrain() { + reader.read().then(flow, handleStreamError); + } + function flow({ done, value }) { + try { + if (done) writable.end(); + else if (!writable.write(value)) writable.once("drain", onDrain); + else return reader.read().then(flow, handleStreamError); + } catch (e) { + handleStreamError(e); + } + } +} +function writeFromReadableStream(stream, writable) { + if (stream.locked) throw new TypeError("ReadableStream is locked."); + else if (writable.destroyed) return; + return writeFromReadableStreamDefaultReader(stream.getReader(), writable); +} +const buildOutgoingHttpHeaders = (headers, defaultContentType) => { + const res = {}; + if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); + if (headers.has("set-cookie")) { + const cookies = []; + for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); + else res[k] = v; + if (cookies.length > 0) res["set-cookie"] = cookies; + } else for (const [k, v] of headers) res[k] = v; + if (defaultContentType) res["content-type"] ??= defaultContentType; + return res; +}; + +//#endregion +//#region src/listener.ts +const outgoingEnded = Symbol("outgoingEnded"); +const incomingDraining = Symbol("incomingDraining"); +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; +const drainIncoming = (incoming) => { + const incomingWithDrainState = incoming; + if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; + incomingWithDrainState[incomingDraining] = true; + if (incoming instanceof Http2ServerRequest) { + try { + incoming.stream?.close?.(constants.NGHTTP2_NO_ERROR); + } catch {} + return; + } + let bytesRead = 0; + const cleanup = () => { + clearTimeout(timer); + incoming.off("data", onData); + incoming.off("end", cleanup); + incoming.off("error", cleanup); + }; + const forceClose = () => { + cleanup(); + const socket = incoming.socket; + if (socket && !socket.destroyed) { + if (typeof socket.destroySoon === "function") socket.destroySoon(); + else if (typeof socket.destroy === "function") socket.destroy(); + } + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref?.(); + const onData = (chunk) => { + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }; + incoming.on("data", onData); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +}; +const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { + if (incoming.errored) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest](incoming.errored.toString()); + } else if (!outgoing.writableFinished || incoming instanceof Http2ServerRequest && incoming.aborted) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest]("Client connection prematurely closed."); + } + if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + drainIncoming(incoming); + }); + }); +}; +const isImmediateCacheableResponse = (res) => { + if (!(cacheKey in res)) return false; + const body = res[cacheKey][1]; + return body === null || typeof body === "string" || body instanceof Uint8Array; +}; +const handleRequestError = () => new Response(null, { status: 400 }); +const handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); +const handleResponseError = (e, outgoing) => { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); + else { + console.error(e); + if (!outgoing.headersSent) { + if (outgoing instanceof ServerResponse) outgoing._contentLength = null; + outgoing.writeHead(500, { "Content-Type": "text/plain" }); + } + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(err); + } +}; +const flushHeaders = (outgoing) => { + if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); +}; +const trySetContentLength = (outgoing, status, length) => { + const http1 = outgoing; + if (http1._contentLength === null && http1._hasBody && http1.useChunkedEncodingByDefault && !http1._removedContLen && status >= 200 && status !== 204 && status !== 304 && !outgoing.hasHeader("content-length") && !outgoing.hasHeader("transfer-encoding") && !outgoing.hasHeader("trailer")) { + http1._contentLength = length; + return true; + } + return false; +}; +const writeDefaultHeaders = (outgoing, status, length) => { + if (trySetContentLength(outgoing, status, length)) outgoing.writeHead(status, { "Content-Type": defaultContentType }); + else outgoing.writeHead(status, { + "Content-Type": defaultContentType, + "Content-Length": length + }); +}; +const responseViaCache = async (res, outgoing) => { + let [status, body, header] = res[cacheKey]; + if (!header) { + if (body === null) { + outgoing.writeHead(status); + outgoing.end(); + } else if (typeof body === "string") { + writeDefaultHeaders(outgoing, status, Buffer.byteLength(body)); + outgoing.end(body); + } else if (body instanceof Uint8Array) { + writeDefaultHeaders(outgoing, status, body.byteLength); + outgoing.end(body); + } else if (body instanceof Blob) { + writeDefaultHeaders(outgoing, status, body.size); + outgoing.end(new Uint8Array(await body.arrayBuffer())); + } else { + outgoing.writeHead(status, { "Content-Type": defaultContentType }); + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); + return; + } + let hasContentLength = false; + let plainHeaders = false; + let canAutoLength = true; + if (header instanceof Headers) { + hasContentLength = header.has("content-length"); + header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); + } else if (Array.isArray(header)) { + const headerObj = new Headers(header); + hasContentLength = headerObj.has("content-length"); + header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); + } else { + plainHeaders = true; + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === "content-length") { + hasContentLength = true; + break; + } + if (key.length === 17 && key.toLowerCase() === "transfer-encoding" || key.length === 7 && key.toLowerCase() === "trailer") canAutoLength = false; + } + } + if (!hasContentLength) { + let length; + if (typeof body === "string") length = Buffer.byteLength(body); + else if (body instanceof Uint8Array) length = body.byteLength; + else if (body instanceof Blob) length = body.size; + if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) { + if (plainHeaders) header = { ...header }; + header["Content-Length"] = length; + } + } + outgoing.writeHead(status, header); + if (body == null) outgoing.end(); + else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); + else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); + else { + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); +}; +const isPromise = (res) => typeof res.then === "function"; +const responseViaResponseObject = async (res, outgoing, options = {}) => { + if (isPromise(res)) if (options.errorHandler) try { + res = await res; + } catch (err) { + const errRes = await options.errorHandler(err); + if (!errRes) return; + res = errRes; + } + else res = await res.catch(handleFetchError); + if (cacheKey in res) return responseViaCache(res, outgoing); + const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); + if (res.body) { + const reader = res.body.getReader(); + const values = []; + let done = false; + let currentReadPromise = void 0; + if (resHeaderRecord["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + currentReadPromise ||= reader.read(); + const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { + console.error(e); + done = true; + }); + if (!chunk) { + if (i === 1) { + await new Promise((resolve) => setTimeout(resolve)); + maxReadCount = 3; + continue; + } + break; + } + currentReadPromise = void 0; + if (chunk.value) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); + } + outgoing.writeHead(res.status, resHeaderRecord); + values.forEach((value) => { + outgoing.write(value); + }); + if (done) outgoing.end(); + else { + if (values.length === 0) flushHeaders(outgoing); + await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); + } + } else if (resHeaderRecord[X_ALREADY_SENT]) {} else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } + outgoing[outgoingEnded]?.(); +}; +const getRequestListener = (fetchCallback, options = {}) => { + const autoCleanupIncoming = options.autoCleanupIncoming ?? true; + if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { + Object.defineProperty(global, "Request", { value: Request$1 }); + Object.defineProperty(global, "Response", { value: Response$1 }); + } + return async (incoming, outgoing) => { + let res, req; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = () => { + if (!req || closeHandlerAttached) return; + closeHandlerAttached = true; + outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); + }; + try { + req = newRequest(incoming, options.hostname); + needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) { + incoming[wrapBodyStream] = true; + if (incoming instanceof Http2ServerRequest) outgoing[outgoingEnded] = () => { + if (!incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + incoming.destroy(); + outgoing.destroy(); + }); + }); + }; + } + res = fetchCallback(req, { + incoming, + outgoing + }); + if (!isPromise(res) && isImmediateCacheableResponse(res)) { + if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { + if (!incoming.readableEnded) drainIncoming(incoming); + }); + return responseViaCache(res, outgoing); + } + ensureCloseHandler(); + } catch (e) { + if (!res) if (options.errorHandler) { + ensureCloseHandler(); + res = await options.errorHandler(req ? e : toRequestError(e)); + if (!res) return; + } else if (!req) res = handleRequestError(); + else res = handleFetchError(e); + else return handleResponseError(e, outgoing); + } + try { + return await responseViaResponseObject(res, outgoing, options); + } catch (e) { + return handleResponseError(e, outgoing); + } + }; +}; + +//#endregion +//#region src/websocket.ts +/** +* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +const CloseEvent = globalThis.CloseEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get wasClean() { + return this.#eventInitDict.wasClean ?? false; + } + get code() { + return this.#eventInitDict.code ?? 0; + } + get reason() { + return this.#eventInitDict.reason ?? ""; + } +}; +/** +* Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`. +* @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent +*/ +const ErrorEvent = globalThis.ErrorEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get message() { + return this.#eventInitDict.message ?? ""; + } + get filename() { + return this.#eventInitDict.filename ?? ""; + } + get lineno() { + return this.#eventInitDict.lineno ?? 0; + } + get colno() { + return this.#eventInitDict.colno ?? 0; + } + get error() { + return this.#eventInitDict.error ?? null; + } +}; +const generateConnectionSymbol = () => Symbol("connection"); +const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); +const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); +const responseHeadersToSkip = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-protocol" +]); +const appendResponseHeaders = (headers, responseHeaders) => { + if (!responseHeaders) return; + responseHeaders.forEach((value, key) => { + if (responseHeadersToSkip.has(key.toLowerCase())) return; + headers.push(`${key}: ${value}`); + }); +}; +const rejectUpgradeRequest = (socket, status, responseHeaders) => { + const responseLines = ["Connection: close", "Content-Length: 0"]; + appendResponseHeaders(responseLines, responseHeaders); + socket.end(`HTTP/1.1 ${status.toString()} ${STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r +`); +}; +const createUpgradeRequest = (request) => { + const protocol = request.socket.encrypted ? "https" : "http"; + const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); + const headers = new Headers(); + for (const key in request.headers) { + const value = request.headers[key]; + if (!value) continue; + headers.append(key, Array.isArray(value) ? value[0] : value); + } + return new Request(url, { headers }); +}; +const setupWebSocket = (options) => { + const { server, fetchCallback, wss } = options; + const waiterMap = /* @__PURE__ */ new Map(); + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiter.resolve(ws); + waiterMap.delete(request); + } + }); + const rejectWaiter = (request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiterMap.delete(request); + waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted")); + } + }; + const waitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve, reject) => { + waiterMap.set(request, { + resolve, + reject, + connectionSymbol + }); + }); + }; + server.on("upgrade", async (request, socket, head) => { + if (request.headers.upgrade?.toLowerCase() !== "websocket") return; + const env = { + incoming: request, + outgoing: void 0, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket + }; + let status = 400; + let responseHeaders; + try { + const response = await fetchCallback(createUpgradeRequest(request), env); + if (response instanceof Response) { + status = response.status; + responseHeaders = response.headers; + } + } catch { + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); + return; + } + const waiter = waiterMap.get(request); + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + rejectWaiter(request); + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); + return; + } + const addResponseHeaders = (headers) => { + appendResponseHeaders(headers, responseHeaders); + }; + const reclaimWaiterOnClose = () => rejectWaiter(request); + socket.once("close", reclaimWaiterOnClose); + wss.on("headers", addResponseHeaders); + try { + wss.handleUpgrade(request, socket, head, (ws) => { + socket.off("close", reclaimWaiterOnClose); + wss.emit("connection", ws, request); + }); + } finally { + wss.off("headers", addResponseHeaders); + } + }); + server.on("close", () => { + wss.close(); + }); +}; +const upgradeWebSocket = defineWebSocketHelper(async (c, events, options) => { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; + const env = c.env; + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; + if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); + const connectionSymbol = generateConnectionSymbol(); + env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + let ws; + try { + ws = await waitForWebSocket(env.incoming, connectionSymbol); + } catch { + return; + } + const messagesReceivedInStarting = []; + const bufferMessage = (data, isBinary) => { + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); + const ctx = { + binaryType: "arraybuffer", + close(code, reason) { + ws.close(code, reason); + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState; + }, + send(source, opts) { + ws.send(source, { compress: opts?.compress }); + }, + url: new URL(c.req.url) + }; + try { + events?.onOpen?.(new Event("open"), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + const handleMessage = (data, isBinary) => { + const datas = Array.isArray(data) ? data : [data]; + for (const data of datas) try { + events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }; + ws.off("message", bufferMessage); + for (const message of messagesReceivedInStarting) handleMessage(...message); + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { + try { + events?.onClose?.(new CloseEvent("close", { + code, + reason: reason.toString() + }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + ws.on("error", (error) => { + try { + events?.onError?.(new ErrorEvent("error", { error }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + })(); + return new Response(); +}); + +//#endregion +//#region src/server.ts +const createAdaptorServer = (options) => { + const fetchCallback = options.fetch; + const requestListener = getRequestListener(fetchCallback, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming + }); + const server = (options.createServer || createServer)(options.serverOptions || {}, requestListener); + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); + setupWebSocket({ + server, + fetchCallback, + wss: options.websocket.server + }); + } + return server; +}; +const serve = (options, listeningListener) => { + const server = createAdaptorServer(options); + server.listen(options?.port ?? 3e3, options.hostname, () => { + const serverInfo = server.address(); + listeningListener && listeningListener(serverInfo); + }); + return server; +}; + +//#endregion +export { RequestError, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs new file mode 100644 index 000000000..bcfb0e52e --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs @@ -0,0 +1,153 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_utils_stream = require('./utils/stream.cjs'); +let hono_utils_mime = require("hono/utils/mime"); +let node_fs = require("node:fs"); +let node_path = require("node:path"); + +//#region src/serve-static.ts +const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i; +const ENCODINGS = { + br: ".br", + zstd: ".zst", + gzip: ".gz" +}; +const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS); +const getStats = (path) => { + let stats; + try { + stats = (0, node_fs.statSync)(path); + } catch {} + return stats; +}; +const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/; +const parseByteRange = (range) => { + const match = range.match(BYTE_RANGE_PATTERN); + if (!match) return; + const [, start, end] = match; + if (start === "") return { + type: "suffix", + length: Number(end) + }; + if (end === "") return { + type: "open-ended", + start: Number(start) + }; + return { + type: "bounded", + start: Number(start), + end: Number(end) + }; +}; +const resolveByteRange = (spec, size) => { + if (size === 0) return; + if (spec.type === "suffix") { + if (spec.length === 0) return; + return { + start: Math.max(size - spec.length, 0), + end: size - 1 + }; + } + const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1; + if (spec.start >= size || spec.start > end) return; + return { + start: spec.start, + end + }; +}; +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; + } + }); + } +}; +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const serveStatic = (options = { root: "" }) => { + const root = options.root || ""; + const optionPath = options.path; + if (root !== "" && !(0, node_fs.existsSync)(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`); + return async (c, next) => { + if (c.finalized) return next(); + if (c.req.method !== "GET" && c.req.method !== "HEAD") return next(); + let filename; + if (optionPath) filename = optionPath; + else try { + if (!options.allowPercentInPath && c.req.path.includes("%")) throw new Error(); + filename = tryDecodeURI(c.req.path); + if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) throw new Error(); + } catch { + await options.onNotFound?.(c.req.path, c); + return next(); + } + let path = (0, node_path.join)(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename); + let stats = getStats(path); + if (stats && stats.isDirectory()) { + const indexFile = options.index ?? "index.html"; + path = (0, node_path.join)(path, indexFile); + stats = getStats(path); + } + if (!stats) { + await options.onNotFound?.(path, c); + return next(); + } + const mimeType = (0, hono_utils_mime.getMimeType)(path); + c.header("Content-Type", mimeType || "application/octet-stream"); + if (options.precompressed && (!mimeType || mimeType === "application/octet-stream" || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { + const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim())); + for (const encoding of ENCODINGS_ORDERED_KEYS) { + if (!acceptEncodingSet.has(encoding)) continue; + const precompressedStats = getStats(path + ENCODINGS[encoding]); + if (precompressedStats) { + c.header("Content-Encoding", encoding); + c.header("Vary", "Accept-Encoding", { append: true }); + stats = precompressedStats; + path = path + ENCODINGS[encoding]; + break; + } + } + } + let result; + const size = stats.size; + const range = c.req.header("range") || ""; + c.header("Last-Modified", stats.mtime.toUTCString()); + if (c.req.method === "HEAD") { + c.header("Content-Length", size.toString()); + c.status(200); + result = c.body(null); + } else if (!range) { + c.header("Content-Length", size.toString()); + result = c.body(require_utils_stream.createStreamBody((0, node_fs.createReadStream)(path)), 200); + } else { + c.header("Accept-Ranges", "bytes"); + const resolvedRange = resolveByteRange(parseByteRange(range) ?? { + type: "open-ended", + start: 0 + }, size); + if (!resolvedRange) { + c.header("Content-Range", `bytes */${size}`); + result = c.body(null, 416); + } else { + const { start, end } = resolvedRange; + const chunkSize = end - start + 1; + const stream = (0, node_fs.createReadStream)(path, { + start, + end + }); + c.header("Content-Length", chunkSize.toString()); + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + result = c.body(require_utils_stream.createStreamBody(stream), 206); + } + } + await options.onFound?.(path, c); + return result; + }; +}; + +//#endregion +exports.serveStatic = serveStatic; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts new file mode 100644 index 000000000..f4dae1201 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts @@ -0,0 +1,19 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/serve-static.d.ts +type ServeStaticOptions = { + /** + * Root path. Relative path is based on current working directory from which the app was started. + */ + root?: string; + path?: string; + index?: string; + precompressed?: boolean; /** Allow percent signs in the routed request path. Defaults to false. */ + allowPercentInPath?: boolean; + rewriteRequestPath?: (path: string, c: Context) => string; + onFound?: (path: string, c: Context) => void | Promise; + onNotFound?: (path: string, c: Context) => void | Promise; +}; +declare const serveStatic: (options?: ServeStaticOptions) => MiddlewareHandler; +//#endregion +export { ServeStaticOptions, serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts new file mode 100644 index 000000000..f4dae1201 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts @@ -0,0 +1,19 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/serve-static.d.ts +type ServeStaticOptions = { + /** + * Root path. Relative path is based on current working directory from which the app was started. + */ + root?: string; + path?: string; + index?: string; + precompressed?: boolean; /** Allow percent signs in the routed request path. Defaults to false. */ + allowPercentInPath?: boolean; + rewriteRequestPath?: (path: string, c: Context) => string; + onFound?: (path: string, c: Context) => void | Promise; + onNotFound?: (path: string, c: Context) => void | Promise; +}; +declare const serveStatic: (options?: ServeStaticOptions) => MiddlewareHandler; +//#endregion +export { ServeStaticOptions, serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs new file mode 100644 index 000000000..97505a187 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs @@ -0,0 +1,152 @@ +import { createStreamBody } from "./utils/stream.mjs"; +import { getMimeType } from "hono/utils/mime"; +import { createReadStream, existsSync, statSync } from "node:fs"; +import { join } from "node:path"; + +//#region src/serve-static.ts +const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i; +const ENCODINGS = { + br: ".br", + zstd: ".zst", + gzip: ".gz" +}; +const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS); +const getStats = (path) => { + let stats; + try { + stats = statSync(path); + } catch {} + return stats; +}; +const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/; +const parseByteRange = (range) => { + const match = range.match(BYTE_RANGE_PATTERN); + if (!match) return; + const [, start, end] = match; + if (start === "") return { + type: "suffix", + length: Number(end) + }; + if (end === "") return { + type: "open-ended", + start: Number(start) + }; + return { + type: "bounded", + start: Number(start), + end: Number(end) + }; +}; +const resolveByteRange = (spec, size) => { + if (size === 0) return; + if (spec.type === "suffix") { + if (spec.length === 0) return; + return { + start: Math.max(size - spec.length, 0), + end: size - 1 + }; + } + const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1; + if (spec.start >= size || spec.start > end) return; + return { + start: spec.start, + end + }; +}; +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; + } + }); + } +}; +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const serveStatic = (options = { root: "" }) => { + const root = options.root || ""; + const optionPath = options.path; + if (root !== "" && !existsSync(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`); + return async (c, next) => { + if (c.finalized) return next(); + if (c.req.method !== "GET" && c.req.method !== "HEAD") return next(); + let filename; + if (optionPath) filename = optionPath; + else try { + if (!options.allowPercentInPath && c.req.path.includes("%")) throw new Error(); + filename = tryDecodeURI(c.req.path); + if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) throw new Error(); + } catch { + await options.onNotFound?.(c.req.path, c); + return next(); + } + let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename); + let stats = getStats(path); + if (stats && stats.isDirectory()) { + const indexFile = options.index ?? "index.html"; + path = join(path, indexFile); + stats = getStats(path); + } + if (!stats) { + await options.onNotFound?.(path, c); + return next(); + } + const mimeType = getMimeType(path); + c.header("Content-Type", mimeType || "application/octet-stream"); + if (options.precompressed && (!mimeType || mimeType === "application/octet-stream" || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { + const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim())); + for (const encoding of ENCODINGS_ORDERED_KEYS) { + if (!acceptEncodingSet.has(encoding)) continue; + const precompressedStats = getStats(path + ENCODINGS[encoding]); + if (precompressedStats) { + c.header("Content-Encoding", encoding); + c.header("Vary", "Accept-Encoding", { append: true }); + stats = precompressedStats; + path = path + ENCODINGS[encoding]; + break; + } + } + } + let result; + const size = stats.size; + const range = c.req.header("range") || ""; + c.header("Last-Modified", stats.mtime.toUTCString()); + if (c.req.method === "HEAD") { + c.header("Content-Length", size.toString()); + c.status(200); + result = c.body(null); + } else if (!range) { + c.header("Content-Length", size.toString()); + result = c.body(createStreamBody(createReadStream(path)), 200); + } else { + c.header("Accept-Ranges", "bytes"); + const resolvedRange = resolveByteRange(parseByteRange(range) ?? { + type: "open-ended", + start: 0 + }, size); + if (!resolvedRange) { + c.header("Content-Range", `bytes */${size}`); + result = c.body(null, 416); + } else { + const { start, end } = resolvedRange; + const chunkSize = end - start + 1; + const stream = createReadStream(path, { + start, + end + }); + c.header("Content-Length", chunkSize.toString()); + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + result = c.body(createStreamBody(stream), 206); + } + } + await options.onFound?.(path, c); + return result; + }; +}; + +//#endregion +export { serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs new file mode 100644 index 000000000..87b8e95ab --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs @@ -0,0 +1,8 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_constants = require('../constants-BXAKTxRC.cjs'); + +//#region src/utils/response.ts +const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [require_constants.X_ALREADY_SENT]: "true" } }); + +//#endregion +exports.RESPONSE_ALREADY_SENT = RESPONSE_ALREADY_SENT; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts new file mode 100644 index 000000000..169a3b551 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts @@ -0,0 +1,4 @@ +//#region src/utils/response.d.ts +declare const RESPONSE_ALREADY_SENT: Response; +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts new file mode 100644 index 000000000..169a3b551 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts @@ -0,0 +1,4 @@ +//#region src/utils/response.d.ts +declare const RESPONSE_ALREADY_SENT: Response; +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs new file mode 100644 index 000000000..92495c1f5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs @@ -0,0 +1,7 @@ +import { t as X_ALREADY_SENT } from "../constants-BLSFu_RU.mjs"; + +//#region src/utils/response.ts +const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [X_ALREADY_SENT]: "true" } }); + +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs new file mode 100644 index 000000000..b4f4f7d42 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs @@ -0,0 +1,65 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +let node_stream = require("node:stream"); +let node_process = require("node:process"); + +//#region src/utils/stream.ts +const pr54206Applied = () => { + const [major, minor] = node_process.versions.node.split(".").map((component) => parseInt(component)); + return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18; +}; +const useReadableToWeb = pr54206Applied(); +const createStreamBody = (stream, useNativeReadableToWeb = useReadableToWeb) => { + if (useNativeReadableToWeb) return node_stream.Readable.toWeb(stream); + let controller; + let settled = false; + const cleanup = () => { + stream.off("data", onData); + stream.off("error", onError); + stream.off("end", onTerminate); + stream.off("close", onTerminate); + }; + const settle = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback?.(); + }; + const onData = (chunk) => { + if (settled || !controller) return; + controller.enqueue(chunk); + if ((controller.desiredSize ?? 0) <= 0) stream.pause(); + }; + const onError = (error) => { + settle(() => { + controller?.error(error); + }); + }; + const onTerminate = () => { + settle(() => { + controller?.close(); + }); + }; + return new ReadableStream({ + start(streamController) { + controller = streamController; + stream.on("data", onData); + stream.on("error", onError); + stream.on("end", onTerminate); + stream.on("close", onTerminate); + stream.pause(); + }, + pull() { + if (!settled) stream.resume(); + }, + cancel() { + settle(); + const ignoreError = () => {}; + stream.on("error", ignoreError); + stream.once("close", () => stream.off("error", ignoreError)); + stream.destroy(); + } + }); +}; + +//#endregion +exports.createStreamBody = createStreamBody; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts new file mode 100644 index 000000000..5f92c4f79 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts @@ -0,0 +1,6 @@ +import { ReadStream } from "node:fs"; + +//#region src/utils/stream.d.ts +declare const createStreamBody: (stream: ReadStream, useNativeReadableToWeb?: boolean) => ReadableStream; +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts new file mode 100644 index 000000000..5f92c4f79 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts @@ -0,0 +1,6 @@ +import { ReadStream } from "node:fs"; + +//#region src/utils/stream.d.ts +declare const createStreamBody: (stream: ReadStream, useNativeReadableToWeb?: boolean) => ReadableStream; +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs new file mode 100644 index 000000000..8d18a9a2e --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs @@ -0,0 +1,64 @@ +import { Readable } from "node:stream"; +import { versions } from "node:process"; + +//#region src/utils/stream.ts +const pr54206Applied = () => { + const [major, minor] = versions.node.split(".").map((component) => parseInt(component)); + return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18; +}; +const useReadableToWeb = pr54206Applied(); +const createStreamBody = (stream, useNativeReadableToWeb = useReadableToWeb) => { + if (useNativeReadableToWeb) return Readable.toWeb(stream); + let controller; + let settled = false; + const cleanup = () => { + stream.off("data", onData); + stream.off("error", onError); + stream.off("end", onTerminate); + stream.off("close", onTerminate); + }; + const settle = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback?.(); + }; + const onData = (chunk) => { + if (settled || !controller) return; + controller.enqueue(chunk); + if ((controller.desiredSize ?? 0) <= 0) stream.pause(); + }; + const onError = (error) => { + settle(() => { + controller?.error(error); + }); + }; + const onTerminate = () => { + settle(() => { + controller?.close(); + }); + }; + return new ReadableStream({ + start(streamController) { + controller = streamController; + stream.on("data", onData); + stream.on("error", onError); + stream.on("end", onTerminate); + stream.on("close", onTerminate); + stream.pause(); + }, + pull() { + if (!settled) stream.resume(); + }, + cancel() { + settle(); + const ignoreError = () => {}; + stream.on("error", ignoreError); + stream.once("close", () => stream.off("error", ignoreError)); + stream.destroy(); + } + }); +}; + +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/package.json b/tests/fixtures/npm/node_modules/@hono/node-server/package.json new file mode 100644 index 000000000..b3ab47d33 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/package.json @@ -0,0 +1,128 @@ +{ + "name": "@hono/node-server", + "version": "2.1.5", + "description": "Node.js Adapter for Hono", + "main": "dist/index.mjs", + "type": "module", + "types": "dist/index.d.mts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./serve-static": { + "import": { + "types": "./dist/serve-static.d.mts", + "default": "./dist/serve-static.mjs" + }, + "require": { + "types": "./dist/serve-static.d.cts", + "default": "./dist/serve-static.cjs" + } + }, + "./utils/*": { + "import": { + "types": "./dist/utils/*.d.mts", + "default": "./dist/utils/*.mjs" + }, + "require": { + "types": "./dist/utils/*.d.cts", + "default": "./dist/utils/*.cjs" + } + }, + "./conninfo": { + "import": { + "types": "./dist/conninfo.d.mts", + "default": "./dist/conninfo.mjs" + }, + "require": { + "types": "./dist/conninfo.d.cts", + "default": "./dist/conninfo.cjs" + } + }, + "./early-hints": { + "import": { + "types": "./dist/early-hints.d.mts", + "default": "./dist/early-hints.mjs" + }, + "require": { + "types": "./dist/early-hints.d.cts", + "default": "./dist/early-hints.cjs" + } + } + }, + "typesVersions": { + "*": { + ".": [ + "./dist/index.d.mts" + ], + "serve-static": [ + "./dist/serve-static.d.mts" + ], + "utils/*": [ + "./dist/utils/*.d.mts" + ], + "conninfo": [ + "./dist/conninfo.d.mts" + ], + "early-hints": [ + "./dist/early-hints.d.mts" + ] + } + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/honojs/node-server.git" + }, + "homepage": "https://github.com/honojs/node-server", + "author": "Yusuke Wada (https://github.com/yusukebe)", + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public", + "provenance": true + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "@hono/eslint-config": "^1.0.1", + "@types/node": "^20.10.0", + "@types/ws": "^8.18.1", + "@whatwg-node/fetch": "^0.9.14", + "eslint": "^9.10.0", + "hono": "^4.13.9", + "np": "^11.2.1", + "prettier": "^3.2.4", + "publint": "^0.3.18", + "tsdown": "^0.20.3", + "typescript": "^5.3.2", + "vitest": "^4.0.18", + "ws": "^8.19.0" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + }, + "scripts": { + "test": "vitest", + "build": "tsdown --external hono", + "watch": "tsdown --watch", + "postbuild": "publint", + "prerelease": "pnpm run build && pnpm run test --run", + "release": "np --no-publish", + "typecheck": "tsc --noEmit", + "lint": "eslint src test", + "lint:fix": "eslint src test --fix", + "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"", + "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"" + } +} \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/LICENSE b/tests/fixtures/npm/node_modules/hono/LICENSE new file mode 100644 index 000000000..bd174facc --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 - present, Yusuke Wada and Hono contributors + +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/hono/dist/helper/websocket/index.js b/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js new file mode 100644 index 000000000..620de4105 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js @@ -0,0 +1,53 @@ +//#region src/helper/websocket/index.ts +/** +* A context for controlling WebSockets +*/ +var WSContext = class { + #init; + constructor(init) { + this.#init = init; + this.raw = init.raw; + this.url = init.url ? new URL(init.url) : null; + this.protocol = init.protocol ?? null; + } + send(source, options) { + this.#init.send(source, options ?? {}); + } + raw; + binaryType = "arraybuffer"; + get readyState() { + return this.#init.readyState; + } + url; + protocol; + close(code, reason) { + this.#init.close(code, reason); + } +}; +const createWSMessageEvent = (source) => { + return new MessageEvent("message", { data: source }); +}; +/** +* Create a WebSocket adapter/helper +*/ +const defineWebSocketHelper = (handler) => { + return ((...args) => { + if (typeof args[0] === "function") { + const [createEvents, options] = args; + return async function upgradeWebSocket(c, next) { + const result = await handler(c, await createEvents(c), options); + if (result) return result; + await next(); + }; + } else { + const [c, events, options] = args; + return (async () => { + const upgraded = await handler(c, events, options); + if (!upgraded) throw new Error("Failed to upgrade WebSocket"); + return upgraded; + })(); + } + }); +}; +//#endregion +export { WSContext, createWSMessageEvent, defineWebSocketHelper }; diff --git a/tests/fixtures/npm/node_modules/hono/package.json b/tests/fixtures/npm/node_modules/hono/package.json new file mode 100644 index 000000000..6264c824a --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/package.json @@ -0,0 +1,12 @@ +{ + "name": "hono", + "version": "4.13.13", + "description": "Test fixture: only the hono/ws helper that @hono/node-server's index imports.", + "type": "module", + "license": "MIT", + "exports": { + "./ws": { + "import": "./dist/helper/websocket/index.js" + } + } +} From 1940e063048c564d7ac5ee7835fe2869f3faf135 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:18:53 -0500 Subject: [PATCH 097/169] Cancel streamed bodies on disconnect and type Response.json headers A response whose client disconnects while its body streams now cancels the body's stream when the response closes, as the package does, so the producer stops instead of being read until it ends. A response that fails closes too and is cancelled the same way. Response.json(data, { headers }) with init headers is written from its Headers, as the package's Response.json passes them on as a Headers object with the JSON content type added when they lack one. Before, the cached path wrote only the init record and sent no Content-Type. The hono-node-server case covers both: Response.json with and without an init content type, and a client that destroys the response after the first chunk of an endless stream. --- .../compiler/shims/hono-node-server/v2.ts | 30 ++++++++--- packages/runtime/src/scr_fetch.c | 6 ++- .../npm/cases/hono-node-server/main.ts | 52 ++++++++++++++++++- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index 3708f0e4c..0402fe0af 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -197,20 +197,34 @@ function nextTick(): Promise { }); } +/** Writes the rest of a body stream. As in the package, a response that + * closes before the stream ends (the client disconnected) cancels the + * stream, so its producer stops; the pending read then answers done. A + * response that fails closes too, and its stream is cancelled the same way + * (the package passes the error as the reason). The listener stays attached + * and does nothing once the stream has ended. */ async function pump( reader: ReadableStreamDefaultReader, outgoing: ServerResponse, pending: Promise | undefined, ): Promise { - let read = pending ?? reader.read(); - for (;;) { - const { done, value } = await read; - if (done) { - outgoing.end(); - return; + let finished = false; + outgoing.on("close", () => { + if (!finished) reader.cancel().catch(() => {}); + }); + try { + let read = pending ?? reader.read(); + for (;;) { + const { done, value } = await read; + if (done) { + outgoing.end(); + return; + } + outgoing.write(value); + read = reader.read(); } - outgoing.write(value); - read = reader.read(); + } finally { + finished = true; } } diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 18daed32f..24cb512b6 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4761,7 +4761,10 @@ static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { * {"content-type": "application/json"} record is form 3), a Headers object * or pair list form 2, and a plain record form 3, whose names and values * *record receives in order (an array value contributes one entry per - * element, as writeHead writes it). 0 for any other init. */ + * element, as writeHead writes it). Response.json with init headers is + * form 2 whatever their shape: the package's Response.json passes them on + * as a Headers object, adding the JSON content type when they lack one. + * 0 for any other init. */ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, ScrArr **record) { if (!headers || headers->kind == SCR_DYN_UNDEF) { @@ -4770,6 +4773,7 @@ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, sf_push_header_text(*record, "content-type", "application/json", 16); return 3; } + if (json_body) return 2; if (headers->kind == SCR_DYN_ARR || (headers->kind == SCR_DYN_HANDLE && headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS)) diff --git a/tests/fixtures/npm/cases/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts index ed732f64e..b47dae8e3 100644 --- a/tests/fixtures/npm/cases/hono-node-server/main.ts +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -2,7 +2,8 @@ // serving a fetch handler: a raw HTTP client prints each exchange's status // line, wire-order headers (Date dropped) and body, covering buffered and // streamed bodies, header forms, cookies, request bodies, URL handling, -// HEAD, empty statuses, and handler errors. +// HEAD, empty statuses, handler errors, and a client that disconnects +// during a streamed body. import * as http from "node:http"; import type { AddressInfo } from "node:net"; import { getRequestListener, RequestError, serve } from "@hono/node-server"; @@ -35,6 +36,27 @@ function readyStream(parts: string[]): ReadableStream { }); } +// A stream that produces until it is cancelled (bounded, so a missed cancel +// ends the program instead of hanging it). +let reportCancel: (reason: string) => void = () => {}; +const endlessCancelled = new Promise((resolve) => { + reportCancel = resolve; +}); +function endlessStream(): ReadableStream { + const encoder = new TextEncoder(); + let pulls = 0; + return new ReadableStream({ + async pull(controller) { + await delay(5); + if (++pulls > 400) controller.close(); + else controller.enqueue(encoder.encode("tick\n")); + }, + cancel(reason) { + reportCancel(`cancelled with ${String(reason)}`); + }, + }); +} + async function handler(request: Request): Promise { const url = new URL(request.url); switch (url.pathname) { @@ -101,6 +123,12 @@ async function handler(request: Request): Promise { return new Response("rec", { status: 201, headers: { "X-Custom": "a", "Content-Type": "text/html" } }); case "/light-json": return Response.json({ ok: true, n: [1, 2] }); + case "/light-json-headers": + return Response.json({ ok: true }, { status: 201, headers: { "X-Test": "yes" } }); + case "/light-json-typed": + return Response.json([1], { headers: { "Content-Type": "application/vnd.test+json" } }); + case "/endless": + return new Response(endlessStream(), { headers: new Headers({ "content-type": "text/plain" }) }); case "/light-touched": { const r = new Response("touched"); r.headers.set("x-touched", "1"); @@ -184,6 +212,8 @@ const exchanges: Exchange[] = [ { method: "GET", path: "/light-record" }, { method: "HEAD", path: "/light-record" }, { method: "GET", path: "/light-json" }, + { method: "GET", path: "/light-json-headers" }, + { method: "GET", path: "/light-json-typed" }, { method: "GET", path: "/light-touched" }, { method: "GET", path: "/light-record-touched" }, { method: "GET", path: "/materialized" }, @@ -198,10 +228,30 @@ const exchanges: Exchange[] = [ { method: "get", path: "/lower-method" }, ]; +/** Reads the first chunk of /endless, then destroys the response: the + * server cancels the response's stream. */ +function disconnectMidStream(port: number): Promise { + return new Promise((resolve) => { + const req = http.request({ host: "127.0.0.1", port, method: "GET", path: "/endless" }, (res) => { + res.once("data", () => { + res.destroy(); + }); + }); + req.end(); + const timeout = new Promise((done) => { + setTimeout(() => done("not cancelled"), 3000); + }); + void Promise.race([endlessCancelled, timeout]).then((outcome) => { + resolve(`### disconnect GET /endless\n${outcome}\n`); + }); + }); +} + const server = serve({ fetch: handler, port: 0, hostname: "127.0.0.1" }, (info: AddressInfo) => { console.log("listening", info.family, info.address, typeof info.port); void (async () => { for (const ex of exchanges) console.log(await exchange(info.port, ex)); + console.log(await disconnectMidStream(info.port)); server.close(() => { console.log("closed"); startSecond(); From 1501559d4a9ac8c2f4b6d86732b85d1cf15c8add Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:34:06 -0500 Subject: [PATCH 098/169] Read and write the "undefined" property for element indexes that are undefined An element index the checker types as a number can hold undefined at run time (an element read from a holey array). JavaScript then names the array's "undefined" property, but the index converted with ToNumber, so such reads and writes used the "NaN" property: Node prints `10 0,7,0,7` for a program that sets `values[NaN] = 10` and then writes through two holes, the compiled program printed `7 0,7,0,7`. The runtime's noncanonical property table, which holds the properties named by non-index numbers, now spells one NaN payload (scr_arr_undefined_key) "undefined". At array element reads, writes, compound and update writes, destructuring targets and writes in value position, an index that can be undefined is that key when it is undefined. The property stays a typed entry of the array, so the Int32[] contract checks cover it like any element store. A typed array cannot hold the property: a write through an undefined index throws the catchable SC1090 error where Node adds it. Fixes #1640. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../containers/array-callback-call.ts | 4 +- .../frontend/lowering/expressions/binary.ts | 43 ++++++-- .../lowering/expressions/element-access.ts | 98 ++++++++++++++--- .../frontend/lowering/expressions/updates.ts | 45 ++++++-- .../src/frontend/lowering/lower-stmts.ts | 4 +- .../src/frontend/lowering/undefined-index.ts | 101 ++++++++++++++++++ .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/ir.ts | 5 +- .../7540-undefined-element-index-keys.ts.json | 6 ++ packages/runtime/src/scr_array.c | 21 +++- packages/runtime/src/scr_array.h | 10 +- .../7540-undefined-element-index-keys.ts | 53 +++++++++ .../7540-undefined-element-index-keys.ts.json | 1 + tests/harness/errors.test.ts | 20 ++++ 15 files changed, 374 insertions(+), 41 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/undefined-index.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json create mode 100644 tests/corpus/7540-undefined-element-index-keys.ts create mode 100644 tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..c3d54aac1 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1583,6 +1583,7 @@ export const LIB_FN_SYMS: Record = { "regex.resultProp": "scr_regex_result_prop", "array.namedKeys": "scr_arr_named_keys", "array.hasNamedKey": "scr_arr_has_named_key", + "array.undefinedKey": "scr_arr_undefined_key", // queueMicrotask's checked-dynamic form (borrowed dyn; a non-function // throws synchronously), the minted setImmediate value, and the // timers/promises immediate — all mark the loop live. diff --git a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts index a22a82bb1..b98000fa1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts @@ -1,10 +1,10 @@ import * as ts from "../../ts7/adapter.js"; +import { lowerOptionalIndex } from "../undefined-index.js"; import { locOf } from "../../program.js"; import { varRef } from "../../../ir/build.js"; import { DYN, type IrExpr, type IrStmt, isUnitType } from "../../../ir/ir.js"; import type { Lowerer } from "../lowerer.js"; import { completeFuncValueArgs } from "../call-arguments.js"; -import { lowerOptionalNumber } from "../lower-exprs.js"; import { lowerSafeIndexRead } from "../array-values.js"; /** Retain native function slots during indexed calls. The array remains @@ -37,7 +37,7 @@ export function lowerArrayCallbackCall( return varRef(local.id, local.type, loc); }; const receiver = store(array, "%callbackArray"); - const index = lowerOptionalNumber( + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(access.argumentExpression), locOf(access.argumentExpression), diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 9702a6dd9..58f6e16b5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,6 +1,11 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; +import { + lowerOptionalIndex, + splitUndefinedIndex, + undefinedTypedArrayKeyFence, +} from "../undefined-index.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; import { @@ -1160,12 +1165,26 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr } if (recv?.type.kind === "array" || recv?.type.kind === "bytes") { const receiver = lowerer.declareHiddenLocal("%setArray", recv.type); - const index = lowerOptionalNumber( - lowerer, - lowerer.lowerExpr(expr.left.argumentExpression), - locOf(expr.left.argumentExpression), - expr.left.argumentExpression, - ); + // An undefined index names the "undefined" property + // (undefined-index.ts), which a typed array cannot hold. + const rawIndex = lowerer.lowerExpr(expr.left.argumentExpression); + const split = + recv.type.kind === "bytes" + ? splitUndefinedIndex( + lowerer, + rawIndex, + expr.left.argumentExpression, + locOf(expr.left.argumentExpression), + ) + : null; + const index = + split?.index ?? + lowerOptionalIndex( + lowerer, + rawIndex, + locOf(expr.left.argumentExpression), + expr.left.argumentExpression, + ); if (index.type.kind !== "f64") lowerer.unsupported( "SC1090", @@ -1178,7 +1197,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr const result = varRef(value.id, raw.type, loc); const arr = varRef(receiver.id, recv.type, loc); const idx = varRef(key.id, F64, loc); - const write: IrStmt = + const store: IrStmt = recv.type.kind === "array" ? arrayValueStore(lowerer, arr, idx, result, recv.type.elem, loc) : { @@ -1188,10 +1207,20 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr value: lowerer.coerceInto(expr.right, result, F64), loc, }; + const write: IrStmt = split + ? { + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, expr.left, loc)], + else_: [store], + loc, + } + : store; return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: receiver.id, init: recv, loc }, + ...(split?.stmts ?? []), { kind: "varDecl", localId: key.id, init: index, loc }, { kind: "varDecl", localId: value.id, init: raw, loc }, write, diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 045bebdf4..16cc8f845 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -1,5 +1,10 @@ /* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */ import * as ts from "../../ts7/adapter.js"; +import { + lowerOptionalIndex, + splitUndefinedIndex, + undefinedTypedArrayKeyFence, +} from "../undefined-index.js"; import { type Lowerer, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js"; import { BOOL, @@ -774,9 +779,9 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } // Array reads can themselves supply an index. A missing numeric value - // is undefined, whose property lookup misses just like NaN; keep that - // value instead of rejecting a checker-number index with optional IR. - const index = lowerOptionalNumber( + // is undefined, which names the array's "undefined" property + // (undefined-index.ts). + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(expr.argumentExpression), locOf(expr.argumentExpression), @@ -1518,12 +1523,26 @@ export function lowerElementCompoundParts( "compound assignment through a non-native array or byte view", ); } - const index = lowerOptionalNumber( - lowerer, - lowerer.lowerExpr(target.argumentExpression), - locOf(target.argumentExpression), - target.argumentExpression, - ); + // An undefined index names the "undefined" property (undefined-index.ts), + // which a typed array cannot hold. + const rawIndex = lowerer.lowerExpr(target.argumentExpression); + const split = + receiver.type.kind === "bytes" + ? splitUndefinedIndex( + lowerer, + rawIndex, + target.argumentExpression, + locOf(target.argumentExpression), + ) + : null; + const index = + split?.index ?? + lowerOptionalIndex( + lowerer, + rawIndex, + locOf(target.argumentExpression), + target.argumentExpression, + ); if (index.type.kind !== "f64") lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type); @@ -1632,15 +1651,25 @@ export function lowerElementCompoundParts( } const resultLocal = lowerer.declareHiddenLocal("%compoundResult", computed.type); const resultRef = (): IrExpr => varRef(resultLocal.id, computed.type, loc); - const write: IrStmt = + const store: IrStmt = receiver.type.kind === "array" ? arrayValueStore(lowerer, receiverRef(), indexRef(), resultRef(), receiver.type.elem, loc) : { kind: "bytesSet", arr: receiverRef(), index: indexRef(), value: resultRef(), loc }; + const write: IrStmt = split + ? { + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, target, loc)], + else_: [store], + loc, + } + : store; return { expr: { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: receiverLocal.id, init: receiver, loc }, + ...(split?.stmts ?? []), { kind: "varDecl", localId: indexLocal.id, init: index, loc }, { kind: "varDecl", localId: oldLocal.id, init: oldValue, loc }, { kind: "varDecl", localId: resultLocal.id, init: computed, loc }, @@ -1867,13 +1896,23 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): // The checker's flow type is a typed array, but the storage is wider. if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv); // An index the checker types as a number but whose storage may hold - // undefined (a hole read) converts with ToNumber, as the reads do. - const index = lowerOptionalNumber( + // undefined (a hole read) converts with ToNumber, as the reads do; an + // undefined index names a property the typed array cannot hold. + const rawIndex = lowerer.lowerExpr(target.argumentExpression); + const split = splitUndefinedIndex( lowerer, - lowerer.lowerExpr(target.argumentExpression), - locOf(target.argumentExpression), + rawIndex, target.argumentExpression, + locOf(target.argumentExpression), ); + const index = + split?.index ?? + lowerOptionalNumber( + lowerer, + rawIndex, + locOf(target.argumentExpression), + target.argumentExpression, + ); if (index.type.kind === "dyn" && recv.type.kind === "bytes") { return { kind: "exprStmt", @@ -1889,7 +1928,34 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys"); } const value = lowerer.lowerExprExpecting(expr.right, F64); - return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) }; + if (split === null) return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) }; + const loc = locOf(expr); + const receiver = lowerer.declareHiddenLocal("%indexedBytes", recv.type); + const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: receiver.id, init: recv, loc }, + ...split.stmts, + { kind: "varDecl", localId: valueLocal.id, init: value, loc }, + { + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, target, loc)], + else_: [ + { + kind: "bytesSet", + arr: varRef(receiver.id, recv.type, loc), + index, + value: varRef(valueLocal.id, F64, loc), + loc, + }, + ], + loc, + }, + ], + loc, + }; } // Tuple element write `t[0] = v`: a positional-field write (recordSet) // — literal indices only, like the read. @@ -2184,7 +2250,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): "assignment through a possibly missing nested array receiver", ); } - const index = lowerOptionalNumber( + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(target.argumentExpression), locOf(target.argumentExpression), diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 128093dad..04ca23a96 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -1,5 +1,10 @@ /* Increment, decrement and prefix unary operators, and compound field updates. */ import * as ts from "../../ts7/adapter.js"; +import { + lowerOptionalIndex, + splitUndefinedIndex, + undefinedTypedArrayKeyFence, +} from "../undefined-index.js"; import { isStrictCode } from "../strict-mode.js"; import { isPrivateWriteReceiver } from "../private-brands.js"; import type { Lowerer } from "../lowerer.js"; @@ -311,12 +316,27 @@ export function lowerIncDec( (raw.type.kind === "array" || raw.type.kind === "bytes") ) { const receiver = save(raw, "%incrementArray"); - const rawIndex = lowerOptionalNumber( - lowerer, - lowerer.lowerExpr(access.argumentExpression), - locOf(access.argumentExpression), - access.argumentExpression, - ); + // An undefined index names the "undefined" property + // (undefined-index.ts), which a typed array cannot hold. + const loweredIndex = lowerer.lowerExpr(access.argumentExpression); + const split = + raw.type.kind === "bytes" + ? splitUndefinedIndex( + lowerer, + loweredIndex, + access.argumentExpression, + locOf(access.argumentExpression), + ) + : null; + if (split) stmts.push(...split.stmts); + const rawIndex = + split?.index ?? + lowerOptionalIndex( + lowerer, + loweredIndex, + locOf(access.argumentExpression), + access.argumentExpression, + ); const index = save( rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc } @@ -340,7 +360,7 @@ export function lowerIncDec( loc, invalidNaN: true, }; - return finish(read, (value) => + const store = (value: IrExpr): IrStmt => type.kind === "array" ? arrayValueStore( lowerer, @@ -356,8 +376,15 @@ export function lowerIncDec( index, value: lowerer.coerceInto(access, value, F64), loc, - }, - ); + }; + if (!split) return finish(read, store); + return finish(read, (value) => ({ + kind: "if", + cond: split.isUndefined, + then: [undefinedTypedArrayKeyFence(lowerer, access, loc)], + else_: [store(value)], + loc, + })); } // A TS member statically typed number on a checked-dynamic receiver (a // class-backed protocol view): the expanded form `o.v = o.v + 1`, its diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 62c2e3a0f..da1bd19ca 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -25,6 +25,7 @@ import { InternalCompilerError } from "../../errors.js"; * flow (if/while/for/for-of/do, switch, try/catch, abrupt completions), * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; +import { lowerOptionalIndex } from "./undefined-index.js"; import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; @@ -204,7 +205,6 @@ import { lowerElementCompoundStatement, lowerIncDec, lowerIncDecTarget, - lowerOptionalNumber, lowerGroupsProjection, matchResultNamedGroupsOf, runtimeOptionalGuardIds, @@ -9279,7 +9279,7 @@ function lowerAssignTargetInto( if (arr.type.kind === "array") { const recv = lowerer.declareHiddenLocal("%dtRecv", arr.type); out.push({ kind: "varDecl", localId: recv.id, init: arr, loc }); - const index = lowerOptionalNumber( + const index = lowerOptionalIndex( lowerer, lowerer.lowerExpr(t.argumentExpression), locOf(t.argumentExpression), diff --git a/packages/compiler/src/frontend/lowering/undefined-index.ts b/packages/compiler/src/frontend/lowering/undefined-index.ts new file mode 100644 index 000000000..5226d7008 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/undefined-index.ts @@ -0,0 +1,101 @@ +import * as ts from "../ts7/adapter.js"; +import { BOOL, DYN, F64, UNDEFINED_T, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import type { Lowerer } from "./lowerer.js"; +import { lowerOptionalNumber } from "./lower-exprs.js"; +import { keyedHelperName } from "./stable-names.js"; + +/** An element index the checker types as a number can be undefined at run + * time (an element read from a holey array). JavaScript then names the + * property "undefined", not "NaN". */ + +/** The array element index `operand` (the lowered index of `node`) as a + * number. An undefined index is the runtime's "undefined" key + * (`array.undefinedKey`, a NaN the array's property table spells + * "undefined"); other values convert with ToNumber, as lowerOptionalNumber + * does. */ +export function lowerOptionalIndex( + lowerer: Lowerer, + operand: IrExpr, + loc: SrcLoc, + node: ts.Expression, +): IrExpr { + const split = splitUndefinedIndex(lowerer, operand, node, loc); + if (split === null) return lowerOptionalNumber(lowerer, operand, loc, node); + return { + kind: "seqExpr", + stmts: split.stmts, + result: { + kind: "ternary", + cond: split.isUndefined, + then: { kind: "libCall", fn: "array.undefinedKey", args: [], type: F64, loc }, + else_: split.index, + type: F64, + loc, + }, + type: F64, + loc, + }; +} + +/** A typed-array element index that can be undefined: `stmts` evaluate it + * once, `isUndefined` tests it, and `index` is the number a defined index + * converts to. */ +export interface SplitIndex { + stmts: IrStmt[]; + isUndefined: IrExpr; + index: IrExpr; +} + +/** Splits `raw`, the lowered index of `node`, when it is a number that can + * be undefined; null otherwise. */ +export function splitUndefinedIndex( + lowerer: Lowerer, + raw: IrExpr, + node: ts.Expression, + loc: SrcLoc, +): SplitIndex | null { + if (raw.type.kind !== "union") return null; + const undefinedTag = lowerer.armTag(raw.type.unionId, UNDEFINED_T); + if (undefinedTag < 0) return null; + const local = lowerer.declareHiddenLocal("%elementIndex", raw.type); + const stable = varRef(local.id, raw.type, loc); + const index = lowerOptionalNumber(lowerer, stable, loc, node); + if (index.type.kind !== "f64") return null; + return { + stmts: [{ kind: "varDecl", localId: local.id, init: raw, loc }], + isUndefined: { + kind: "unionIsTag", + unionId: raw.type.unionId, + tag: undefinedTag, + negated: false, + value: stable, + type: BOOL, + loc, + }, + index, + }; +} + +/** A typed array has no property named "undefined" to store into: the + * store throws the catchable SC1090 error when it runs. A call, so that it + * can sit among an expression's statements. */ +export function undefinedTypedArrayKeyFence(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrStmt { + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1; + const message = `typed array properties named 'undefined' (an element index that is undefined) are not supported yet [SC1090 at ${loc.file}:${line}]`; + const key = `index.undefinedFence:${message}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%index.undefinedFence", key); + lowerer.arrHofHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [{ kind: "runtimeFence", code: "SC1090", message, loc }], + loc, + }); + } + return { kind: "exprStmt", expr: { kind: "call", callee: name, args: [], type: DYN, loc }, loc }; +} diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..51a0ec778 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3813,6 +3813,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // Whether the array (args[0]) still has the key (args[1]) from its // namedKeys list. Borrowed; never throws. "array.hasNamedKey": { argTypes: [null, STRING], result: BOOL }, + // The key of an element access whose index is undefined (the property + // named "undefined"; frontend/lowering/undefined-index.ts). + "array.undefinedKey": { argTypes: [], result: F64 }, // node:events EventEmitter: receivers are emitter-hierarchy objects and // the chaining forms (on/off/removeAll/setMax) return the receiver's // own class — program-dependent object types, checked in the libCall diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 0fa873a16..8c3ead512 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2099,8 +2099,9 @@ export type IrStmt = * and whose `code` carries the SC diagnostic code; unwinds exactly like * `throw`. * TypeScript sources produce it only as the tsc-unreachable fallthrough - * trap (appendImplicitUndefinedReturn's SC9002) — their construct fences - * stay compile errors. */ + * trap (appendImplicitUndefinedReturn's SC9002) and for a typed-array + * store through an element index that is undefined at run time + * (undefined-index.ts); their construct fences stay compile errors. */ | { kind: "runtimeFence"; code: string; message: string; loc: SrcLoc } /** Rethrow of a catch binding (`throw e` where e is the binding): * re-raises the SAVED exception exactly — kind and payload preserved, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json new file mode 100644 index 000000000..528758c4e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7540-undefined-element-index-keys.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index bbe66e26e..f4092e75b 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -176,7 +176,22 @@ static size_t scr_arr_prop_index(const ScrArr *a, const char *key, return SIZE_MAX; } +#define SCR_ARR_UNDEFINED_KEY_BITS UINT64_C(0x7ff8000000756e64) + +double scr_arr_undefined_key(void) { + uint64_t bits = SCR_ARR_UNDEFINED_KEY_BITS; + double key; + memcpy(&key, &bits, sizeof key); + return key; +} + static size_t scr_arr_prop_key(double i, char *buf) { + uint64_t bits; + memcpy(&bits, &i, sizeof bits); + if (bits == SCR_ARR_UNDEFINED_KEY_BITS) { + memcpy(buf, "undefined", sizeof "undefined"); + return sizeof "undefined" - 1; + } return scr_f64_to_str(i, buf); } @@ -2012,7 +2027,7 @@ static uint64_t scr_arr_require_slot(ScrArr *a, double i) { if (scr_arr_prop_get_state(a, i, &prop, &state)) { if (state == SCR_ARR_UNDEFINED) { char buf[32]; - scr_f64_to_str(i, buf); + scr_arr_prop_key(i, buf); scr_trap_fmt("scriptc: RangeError: numeric property %s is undefined\n", buf); } return prop; @@ -5121,6 +5136,10 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { bool scr_arr_numeric_key(const char *key, size_t len, double *out) { char text[64]; + if (len == sizeof "undefined" - 1 && memcmp(key, "undefined", len) == 0) { + *out = scr_arr_undefined_key(); + return true; + } if (len == 0 || len >= sizeof text) return false; memcpy(text, key, len); text[len] = '\0'; diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6034462e9..1750e8509 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -457,9 +457,15 @@ void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); * properties of `target`, so neither is lost when the storage is swapped. */ void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target); /* True when `key` is the string spelling of a number that is not an array - * index ("-1", "1.5", "NaN", "Infinity"), with the number in *out. Arrays - * keep such properties in their noncanonical property table. */ + * index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in + * *out (scr_arr_undefined_key for "undefined"). Arrays keep such properties + * in their noncanonical property table. */ bool scr_arr_numeric_key(const char *key, size_t len, double *out); +/* The number compiled code passes as the key of an element access whose + * index is undefined at run time: a NaN with its own payload, which the + * property table spells "undefined" (JavaScript names that property, not + * "NaN"). */ +double scr_arr_undefined_key(void); /* Whether the array still has the named or noncanonical numeric property * `key`: `for...in` skips a key deleted before its turn. Borrows both. */ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key); diff --git a/tests/corpus/7540-undefined-element-index-keys.ts b/tests/corpus/7540-undefined-element-index-keys.ts new file mode 100644 index 000000000..8da1b591b --- /dev/null +++ b/tests/corpus/7540-undefined-element-index-keys.ts @@ -0,0 +1,53 @@ +// An element index the checker types as a number can be undefined at run +// time (an element read from a holey array). JavaScript then names the +// array's "undefined" property, not "NaN": plain, compound, update and +// destructuring writes store it, a write used as a value yields the value, +// and reads (number, string, record and array elements) read it back. +const holey: number[] = [1]; +holey[3] = 3; + +const values: number[] = [0, 0, 0, 0]; +values[NaN] = 10; +for (const i of holey) values[i] = 7; +console.log(values[NaN], values.join(",")); +let total = 0; +for (const i of holey) total += values[i]!; +console.log(total); + +const counts: number[] = [0, 0, 0, 0]; +counts[NaN] = 10; +let trace = ""; +for (const i of holey) trace += counts[i]++ + ","; +for (const i of holey) counts[i] += 2; +for (const i of holey) trace += --counts[i] + ";"; +console.log(counts[NaN], counts.join(","), trace); + +let last = 0; +for (const i of holey) last = values[i] = 5; +for (const i of holey) [values[i]] = [8]; +console.log(last, values[NaN], values.join(",")); + +const names: string[] = ["a", "b", "c", "d"]; +for (const i of holey) names[i] += "!"; +const seen: (string | undefined)[] = []; +for (const i of holey) seen.push(names[i]); +console.log(names.join("|"), seen.join("|")); + +interface Point { + x: number; +} +const points: Point[] = [{ x: 0 }, { x: 1 }]; +const shared: Point = { x: 9 }; +let same = ""; +for (const i of holey) { + points[i] = shared; + same += String(points[i] === shared) + " "; +} +shared.x = 10; +for (const i of holey) same += String(points[i]?.x) + " "; +console.log(same); + +const fresh: number[] = [1, 2]; +let missing = ""; +for (const i of holey) missing += String(fresh[i]) + " "; +console.log(missing); diff --git a/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json b/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json new file mode 100644 index 000000000..1da1b737f --- /dev/null +++ b/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 5300 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8b0b51b8..566a28e96 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -305,6 +305,26 @@ describe("checked catch-binding casts", () => { ); }); + test("a typed-array write through an undefined index is a named runtime fence", async () => { + // Node stores an ordinary property named "undefined" on the typed + // array, which a compiled typed array cannot hold; defined indexes + // before it store as usual. + const r = await compileAndRun( + "typed-array-undefined-index", + `const holey: number[] = [1]; +holey[3] = 3; +const cells = new Float64Array(4); +for (const i of holey) cells[i] = 9; +console.log(cells.join(",")); +`, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(""); + expect(r.stderr).toMatch( + /^Uncaught Error: typed array properties named 'undefined' \(an element index that is undefined\) are not supported yet \[SC1090 at .*typed-array-undefined-index\.ts:4\]\n$/, + ); + }); + test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => { // Effect-free surplus args drop at compile time, JS-exact (corpus // 1760 — the test-string-decoder writeSequences shape). An effectful From 14966de563791e76de1bd455450e496e0838ee0f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:35:40 -0700 Subject: [PATCH 099/169] Move adoption into its own runtime unit behind the declAdopt feature Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 1 + .../src/backend/native-link-info-core.ts | 1 + .../compiler/src/backend/native/contracts.ts | 1 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../src/executable/executable-cache.ts | 1 + .../src/frontend/decl-projection.test.ts | 19 +- .../compiler/src/frontend/decl-projection.ts | 80 ++++++-- .../frontend/lowering/coercions/structural.ts | 10 +- .../lowering/expressions/narrowing.ts | 20 +- packages/compiler/src/frontend/pipeline.ts | 12 +- packages/compiler/src/frontend/program.ts | 19 +- packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/runtime-features.ts | 5 + .../compiler/src/native/sanitizer.test.ts | 1 + .../npm/cases/types-boundary/main.ts.json | 6 + .../npm/cases/zod3-adoption/main.ts.json | 6 + .../npm/cases/zod3-boundary/main.ts.json | 6 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_decl_adopt.c | 149 +++++++++++++++ packages/runtime/src/scr_dyn.h | 5 - packages/runtime/src/scr_json.c | 176 ++---------------- 22 files changed, 317 insertions(+), 207 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json create mode 100644 packages/runtime/src/scr_decl_adopt.c diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 672fddd96..32318d53e 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.handleProps ? { handleProps: true } : {}), + ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 5782f091d..e018037fd 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -49,6 +49,7 @@ export interface NativeLinkFeatures { /** Own properties JavaScript adds to children: scr_dyn_handle_props.c. * Absent (false) for programs without them. */ handleProps?: boolean; + declAdopt?: boolean; /** for await over checked-dynamic values: scr_dyn_for_await.c. Absent * (false) for programs without it. */ dynForAwait?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index f683ede3e..61de82e72 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -122,6 +122,7 @@ export interface CcOptions { streamDyn?: boolean; /** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */ handleProps?: boolean; + declAdopt?: boolean; /** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */ dynForAwait?: boolean; /** events.once with options (events.onceSignal): compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 1b90bb1f1..233b9cac2 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -279,6 +279,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"), // Own properties JavaScript adds to children. unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"), + // The declaration-typed boundary's adoption. + unit((u) => u.opts.declAdopt === true, "scr_decl_adopt.c"), // for await over checked-dynamic values. unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"), // for await over checked-dynamic values that may hold native streams. diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index b59fdc093..75953be59 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -86,6 +86,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { dynTier: boolean; streamDyn: boolean; handleProps: boolean; + declAdopt: boolean; dynForAwait: boolean; eventsOnceSignal: boolean; dynBuffer: boolean; @@ -283,6 +284,7 @@ export function effectiveRuntimeFeatures( dynTier: features.dynTier === true, streamDyn: features.streamDyn === true, handleProps: features.handleProps === true, + declAdopt: features.declAdopt === true, dynForAwait: features.dynForAwait === true, eventsOnceSignal: features.eventsOnceSignal === true, dynBuffer: features.dynBuffer === true, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 5c3904a91..7b7febc06 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -70,6 +70,7 @@ export interface EarlyExecutableNativeFeatures { dynTier?: boolean; streamDyn?: boolean; handleProps?: boolean; + declAdopt?: boolean; dynForAwait?: boolean; eventsOnceSignal?: boolean; dynBuffer?: boolean; diff --git a/packages/compiler/src/frontend/decl-projection.test.ts b/packages/compiler/src/frontend/decl-projection.test.ts index ef8771b06..dee695dd8 100644 --- a/packages/compiler/src/frontend/decl-projection.test.ts +++ b/packages/compiler/src/frontend/decl-projection.test.ts @@ -2,17 +2,16 @@ import { describe, expect, test } from "vitest"; import { applyEdits, isDeclBoundaryFile, isDeclBoundaryTypeName } from "./decl-projection.js"; describe("decl-projection", () => { - test("applyEdits nests wraps that close at one position", () => { - const text = "const p = a.b;"; - // Outer wrap of `a.b` and inner wrap of `a`... closing at different ends; - // two wraps closing at the same position close the inner one first. - const out = applyEdits(text, [ - { start: 10, end: 10, text: "(" }, - { start: 13, end: 13, text: " as Outer)", rank: -10 }, - { start: 10, end: 10, text: "(" }, - { start: 13, end: 13, text: " as Inner)", rank: -9 }, + test("applyEdits closes the wrap that opened last first", () => { + // `x.y` wrapped from 0 (outer) and from 2 (inner), both closing at 5: + // closing parts rank by their wrap's negated start. + const out = applyEdits("x = a.b;", [ + { start: 4, end: 4, text: "(" }, + { start: 7, end: 7, text: " as Outer)", rank: -4 }, + { start: 6, end: 6, text: "(" }, + { start: 7, end: 7, text: " as Inner)", rank: -6 }, ]); - expect(out).toBe("const p = ((a.b as Inner) as Outer);"); + expect(out).toBe("x = (a.(b as Inner) as Outer);"); }); test("applyEdits keeps the first of overlapping replacements and every line", () => { diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 923bfb428..4ff843c8b 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -316,7 +316,8 @@ function isWriteTarget(node: ts.Node): boolean { return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken; } if (ts.isDeleteExpression(parent)) return true; - if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) return parent.initializer === child; + if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) + return parent.initializer === child; if (ts.isShorthandPropertyAssignment(parent)) return true; if ( ts.isArrayLiteralExpression(parent) || @@ -325,9 +326,19 @@ function isWriteTarget(node: ts.Node): boolean { ) { // Destructuring assignment targets: conservative, skip. let up: ts.Node | undefined = parent; - while (up !== undefined && (ts.isArrayLiteralExpression(up) || ts.isObjectLiteralExpression(up) || ts.isPropertyAssignment(up) || ts.isSpreadElement(up))) + while ( + up !== undefined && + (ts.isArrayLiteralExpression(up) || + ts.isObjectLiteralExpression(up) || + ts.isPropertyAssignment(up) || + ts.isSpreadElement(up)) + ) up = up.parent; - if (up !== undefined && ts.isBinaryExpression(up) && up.operatorToken.kind === ts.SyntaxKind.EqualsToken) + if ( + up !== undefined && + ts.isBinaryExpression(up) && + up.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) return true; } return false; @@ -469,21 +480,27 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => { if (!ts.isExpression(jsNode)) return; const isCall = - ts.isCallExpression(jsNode) || ts.isNewExpression(jsNode) || ts.isTaggedTemplateExpression(jsNode); - const isAccess = ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); + ts.isCallExpression(jsNode) || + ts.isNewExpression(jsNode) || + ts.isTaggedTemplateExpression(jsNode); + const isAccess = + ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); const isAwait = ts.isAwaitExpression(jsNode); if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return; if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return; if (inOptionalChain(jsNode)) return; const parent = jsNode.parent; if (parent !== undefined && ts.isExpressionStatement(parent)) return; - if (parent !== undefined && (ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent))) + if ( + parent !== undefined && + (ts.isImportSpecifier(parent) || + ts.isImportClause(parent) || + ts.isNamespaceImport(parent) || + ts.isExportSpecifier(parent)) + ) return; if (ts.isIdentifier(jsNode)) { - if ( - parent !== undefined && - (ts.isPropertyAccessExpression(parent) && parent.name === jsNode) - ) + if (parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.name === jsNode) return; } const jsType = typeAt(jsChecker, jsNode); @@ -556,7 +573,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const annotated = new Set(); const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => { - if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) return; + if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) + return; const fn = jsNode.parent; if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return; if (jsNode.dotDotDotToken !== undefined) return; @@ -570,7 +588,13 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression); if (ctxFn === undefined) return; const sigs = declChecker.getCallSignatures(ctxFn); - if (!sigs.some((s) => s.declaration !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)))) + if ( + !sigs.some( + (s) => + s.declaration !== undefined && + isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)), + ) + ) return; } catch { return; @@ -582,7 +606,10 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const nameStart = ts.tokenStartOfNode(name); const text = jsSf.text; const bareArrow = - ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && text[ts.tokenStartOfNode(fn)] !== "(" && text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === "" ; + ts.isArrowFunction(fn) && + fn.parameters.nodes.length === 1 && + text[ts.tokenStartOfNode(fn)] !== "(" && + text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === ""; const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; if (bareArrow) { fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); @@ -633,7 +660,11 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const decls = sym.declarations; if (decls.length === 1) { const decl = decls[0]!; - if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined && decl.type === undefined) + if ( + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + decl.type === undefined + ) result = willBeTyped(decl.initializer, depth + 1); else if (ts.isParameter(decl) && annotated.has(decl)) result = true; else if (ts.isBindingElement(decl)) result = false; @@ -675,10 +706,17 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | return found; }; for (const e of kept) { - if (!ts.isAwaitExpression(e) && containsKept(e) && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))) { + if ( + !ts.isAwaitExpression(e) && + containsKept(e) && + (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) + ) { // `a.b.c` with `a.b` wrapped: the outer access reads typed data. const recv = receiverOf(e); - if (recv !== null && (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv))) + if ( + recv !== null && + (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv)) + ) continue; } const alias = exprCandidates.get(e)!; @@ -689,13 +727,19 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // semicolon continues the previous expression: guard it. let up: ts.Node | undefined = e; while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; - if (up?.parent !== undefined && ts.isExpressionStatement(up.parent) && ts.tokenStartOfNode(up.parent) === start) + if ( + up?.parent !== undefined && + ts.isExpressionStatement(up.parent) && + ts.tokenStartOfNode(up.parent) === start + ) prefix = ";("; void stmt; fileEdits.push({ start, end: start, text: prefix }); // Closing parts at one position close inner wraps first. fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start }); - plan.push(`${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`); + plan.push( + `${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`, + ); } if (fileEdits.length === 0) continue; diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 58992da54..4321a5ead 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -290,7 +290,11 @@ export function applyWidthLift( throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot"); // The declaration-typed boundary keeps user values' identity when // they cross into package code (decl-projection.ts). - if (lowerer.declBoundary && !/[\\/]node_modules[\\/]/.test(loc.file) && liveBoxable(lowerer, value.type)) + if ( + lowerer.declBoundary && + !/[\\/]node_modules[\\/]/.test(loc.file) && + liveBoxable(lowerer, value.type) + ) return { kind: "dynFrom", value, liveRef: true, type: DYN, loc }; return { kind: "dynFrom", value, type: DYN, loc }; } @@ -790,5 +794,7 @@ function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set()): boo if (t.kind === "array") return true; if (t.kind !== "union" || seen.has(t.unionId)) return false; seen.add(t.unionId); - return lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false; + return ( + lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false + ); } diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index d0064e56c..fa8ad686f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -1618,7 +1618,9 @@ function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): if (t.kind === "record" || t.kind === "array") return true; if (t.kind !== "union" || seen.has(t.unionId)) return false; seen.add(t.unionId); - return lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false; + return ( + lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false + ); } /** `( as __scriptc_decl_N)`: check the dynamic value against @@ -1654,7 +1656,13 @@ function declBoundaryCast( { kind: "varDecl", localId: value.id, - init: { kind: "dynCheck", value: varRef(source.id, DYN, loc), adopt: true, type: target, loc }, + init: { + kind: "dynCheck", + value: varRef(source.id, DYN, loc), + adopt: true, + type: target, + loc, + }, loc, }, { @@ -1664,7 +1672,13 @@ function declBoundaryCast( fn: "dyn.declAdopt", args: [ varRef(source.id, DYN, loc), - { kind: "dynFrom", value: varRef(value.id, target, loc), liveRef: true, type: DYN, loc }, + { + kind: "dynFrom", + value: varRef(value.id, target, loc), + liveRef: true, + type: DYN, + loc, + }, ], type: VOID, loc, diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 193e8adc0..6da81dfbc 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -685,8 +685,8 @@ function loadFrontend( ...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile), ].map((sf) => [sf.fileName, sf.text]), ), - lower: (opts) => - lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { + lower: (opts) => { + const lowered = lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, @@ -695,7 +695,13 @@ function loadFrontend( projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, - }), + }); + // The declaration-typed boundary's notes (SC6102) join the + // performance warnings; they never fail a build. + const notes = finalLoad.declBoundaryNotes; + if (notes === undefined) return lowered; + return { ...lowered, performance: [...notes, ...(lowered.performance ?? [])] }; + }, npmStatic: statuses, npmImportSites: npmSites, dispose: finalLoad.dispose, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index ef5e46d50..68b649b07 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -387,6 +387,9 @@ export interface LoadResult { * checkPreflight. */ startupCrash?: StartupCrash | null; configDiags: ScrDiagnostic[]; + /** SC6102 performance notes of the declaration-typed boundary + * (decl-projection.ts): files whose projection was dropped. */ + declBoundaryNotes?: ScrDiagnostic[]; /** The files whose JSX the program lowers (jsx-source.ts), by file name. * The program holds their rewritten text; positions in diagnostics refer * to it. */ @@ -932,6 +935,7 @@ function loadProgram7( // lowering compiles holds them with their JSX lowered. const jsx = lowerProgramJsx7(program, options); let tscWorld: LoadResult["tscWorld"]; + const declBoundaryNotes: ScrDiagnostic[] = []; if (jsx.refusal !== null) { config.diags.push(jsx.refusal); } else if (jsx.sources.size > 0) { @@ -977,9 +981,7 @@ function loadProgram7( if (dropped.length > 0) { program.dispose(); program = host.createProgram([...programRoots, overridesDtsPath()], options); - for (const file of dropped) { - config.diags.push(declBoundaryDroppedDiag(file)); - } + for (const file of dropped) declBoundaryNotes.push(declBoundaryDroppedDiag(file)); } if (dropped.length === projection.files.size) { program.dispose(); @@ -1004,6 +1006,7 @@ function loadProgram7( entry, moduleOrder: [], configDiags: config.diags, + ...(declBoundaryNotes.length > 0 ? { declBoundaryNotes } : {}), typeWorld: { nodeFallback: nodeTypes === null, projectLib: config.projectLib ?? null, @@ -1299,7 +1302,15 @@ function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]) continue; if (name.endsWith(".tsx")) continue; // JSX files: not projected yet const text = sf.text; - if (packages.some((pkg) => text.includes(`"${pkg}"`) || text.includes(`'${pkg}'`) || text.includes(`"${pkg}/`) || text.includes(`'${pkg}/`))) + if ( + packages.some( + (pkg) => + text.includes(`"${pkg}"`) || + text.includes(`'${pkg}'`) || + text.includes(`"${pkg}/`) || + text.includes(`'${pkg}/`), + ) + ) out.push(sf.fileName); } return out; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index f0f19a408..26ed4defb 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -600,6 +600,7 @@ async function compileExecutableNative( ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.handleProps ? { handleProps: true } : {}), + ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85910369a..819cfbee0 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -858,6 +858,10 @@ const RUNTIME_FEATURE_RULES = { }), /** Own properties JavaScript adds to children (scr_dyn_handle_props.c). */ handleProps: rule({ fns: ["child.propsInstall"] }), + /** The declaration-typed boundary's adoption (scr_decl_adopt.c): its + * casts, and closures boxed in a boundary program, which adopt their + * arguments. */ + declAdopt: rule({ fns: ["dyn.declAdopt"], initial: (mod) => mod.declBoundary === true }), /** for await over checked-dynamic values (scr_dyn_for_await.c). */ dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }), /** Buffer.prototype methods called on checked-dynamic values @@ -1081,6 +1085,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stream: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, dynBuffer: false, builtinValues: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 49867f0ad..4c031a8e7 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -79,6 +79,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dynTier: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, eventsOnceSignal: false, dynBuffer: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json new file mode 100644 index 000000000..0ef25fcbd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json new file mode 100644 index 000000000..72fa6d390 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-adoption/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json new file mode 100644 index 000000000..4b8187352 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index d0194be05..285e767f4 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -127,6 +127,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_stream_dyn.c", "streamDyn"], // Own properties JavaScript adds to children. ["scr_dyn_handle_props.c", "handleProps"], + ["scr_decl_adopt.c", "declAdopt"], // for await over checked-dynamic values. ["scr_dyn_for_await.c", "dynForAwait"], ["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")], diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c new file mode 100644 index 000000000..3173ffb84 --- /dev/null +++ b/packages/runtime/src/scr_decl_adopt.c @@ -0,0 +1,149 @@ +/* The declaration-typed boundary's adoption, linked only by programs with + * boundary casts (runtime feature declAdopt). + * + * A boundary cast (decl-projection.ts in the compiler) checks a value + * package code produced against the type the package's declarations give + * it and builds the native value. Copying would be observable whenever + * something else still references a source object or array, so this pass + * walks the source next to a live capsule of the built value and turns + * every source node with another holder (rc above one) into a live + * capsule of the native value built from it: every holder then reads and + * writes the same native value. Unshared nodes are left to be released + * with their parent, which no program can tell apart from a move. + * + * Phase one checks that every converted node is plain data (the native + * value has no prototype, accessors, symbol keys or frozen state) and + * collects the shared ones; it throws the path TypeError before anything + * changes. Phase two converts the shared nodes in place + * (scr_dyn_decl_become, scr_json.c). */ +#include "scr_runtime.h" + +#include +#include + +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); + +typedef struct { + ScrDyn *node; + ScrDyn *capsule; /* owned */ +} ScrDeclAdopt; + +typedef struct { + ScrDeclAdopt *items; + size_t len, cap; +} ScrDeclAdoptList; + +static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { + if (list->len == list->cap) { + list->cap = list->cap ? list->cap * 2 : 8; + list->items = realloc(list->items, list->cap * sizeof *list->items); + if (!list->items) scr_trap("scriptc: out of memory\n"); + } + list->items[list->len].node = node; + list->items[list->len].capsule = capsule; + list->len++; +} + +static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { + const char *want = NULL; + if (n->kind == SCR_DYN_OBJ) { + if (n->prototype || n->null_proto || n->v.obj.class_name) + want = "a plain object (this value has a prototype)"; + else if (n->symbol_properties || n->symbol_keys) + want = "a plain object (this value has symbol-keyed properties)"; + else if (n->non_extensible) + want = "a plain object (this value is frozen, sealed or not extensible)"; + else if (n->v.obj.source_access && !n->v.obj.source_identity) + want = "a plain object"; + else { + for (size_t i = 0; i < n->v.obj.len && !want; i++) { + const ScrDynEntry *e = &n->v.obj.entries[i]; + if (e->accessor) want = "a plain object (this value has accessor properties)"; + else if (!e->writable || !e->enumerable || !e->configurable) + want = "a plain object (this value has non-default property attributes)"; + } + } + } else if (n->kind == SCR_DYN_ARR) { + if (n->prototype) + want = "a plain array (this value has a prototype)"; + else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) + want = "a plain array (this value has extra properties)"; + else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) + want = "a plain array (this value is frozen, sealed or not extensible)"; + } + if (!want) return true; + scr_dyn_check_fail(path, want, n); + return false; +} + +static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { + for (size_t i = 0; i < list->len; i++) + if (list->items[i].node == node) return list->items[i].capsule; + return NULL; +} + +/* `capsule` is owned by the call: pushed or released. */ +static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, + const ScrDynPath *path, unsigned depth) { + if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || + capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { + scr_dyn_release(capsule); + return true; + } + if (!scr_decl_adopt_plain(n, path)) { + scr_dyn_release(capsule); + return false; + } + if (n->rc > 1 && n->rc != SIZE_MAX) { + ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); + if (seen) { + bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; + scr_dyn_release(capsule); + if (!same) + scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); + return same; + } + scr_decl_adopt_push(list, n, capsule); + } + void *ptr = capsule->v.typed_ref.ptr; + if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; + for (size_t i = 0; i < ops->count; i++) { + if (!ops->has(ptr, i)) continue; + const ScrStr *name = ops->names[i]; + ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, name->data, 0 }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && + capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; + for (size_t i = 0; i < n->v.arr.len; i++) { + ScrDyn *child = n->v.arr.items[i]; + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return true; +} + +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { + ScrDeclAdoptList list = { NULL, 0, 0 }; + bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); + for (size_t i = 0; i < list.len; i++) { + ScrDeclAdopt *item = &list.items[i]; + if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_dyn_decl_become(item->node, item->capsule); + scr_dyn_release(item->capsule); + } + free(list.items); +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 987de7758..28c2accce 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1000,11 +1000,6 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); * retained view released with the record. Its capsules adopt that view, and * the typed serializers consult `has` to serialize through the capsule. * `apply` consumes a fresh copying snapshot and returns it merged (+1). */ -/* The declaration-typed boundary's adoption (scr_json.c): every object or - * array of `source` that something else still references becomes a live - * capsule of the native value `capsule` built from it. Throws the path - * TypeError, changing nothing, when a converted node is not plain data. */ -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); 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); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 336d42e90..dea3426e6 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15756,138 +15756,13 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) { return scr_dyn_object_proto_name(key); } -/* ── declaration-typed boundary: adoption ─────────────────────────────── - * A boundary cast (decl-projection.ts in the compiler) checks a value - * package code produced against the type the package's declarations give - * it and builds the native value. Copying would be observable whenever - * something else still references a source object or array, so this pass - * walks the source next to a live capsule of the built value and turns - * every source node with another holder (rc above one) into a live - * capsule of the native value built from it: every holder then reads and - * writes the same native value. Unshared nodes are left to be released - * with their parent, which no program can tell apart from a move. - * - * Phase one checks that every converted node is plain data (the native - * value has no prototype, accessors, symbol keys or frozen state) and - * collects the shared ones; it throws the path TypeError before anything - * changes. Phase two converts the shared nodes in place. Programs without - * boundary casts never reference this code (the linker drops it). */ -typedef struct { - ScrDyn *node; - ScrDyn *capsule; /* owned */ -} ScrDeclAdopt; - -typedef struct { - ScrDeclAdopt *items; - size_t len, cap; -} ScrDeclAdoptList; - -static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { - if (list->len == list->cap) { - list->cap = list->cap ? list->cap * 2 : 8; - list->items = realloc(list->items, list->cap * sizeof *list->items); - if (!list->items) scr_trap("scriptc: out of memory\n"); - } - list->items[list->len].node = node; - list->items[list->len].capsule = capsule; - list->len++; -} - -static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { - const char *want = NULL; - if (n->kind == SCR_DYN_OBJ) { - if (n->prototype || n->null_proto || n->v.obj.class_name) - want = "a plain object (this value has a prototype)"; - else if (n->symbol_properties || n->symbol_keys) - want = "a plain object (this value has symbol-keyed properties)"; - else if (n->non_extensible) - want = "a plain object (this value is frozen, sealed or not extensible)"; - else if (n->v.obj.source_access && !n->v.obj.source_identity) - want = "a plain object"; - else { - for (size_t i = 0; i < n->v.obj.len && !want; i++) { - const ScrDynEntry *e = &n->v.obj.entries[i]; - if (e->accessor) want = "a plain object (this value has accessor properties)"; - else if (!e->writable || !e->enumerable || !e->configurable) - want = "a plain object (this value has non-default property attributes)"; - } - } - } else if (n->kind == SCR_DYN_ARR) { - if (n->prototype) - want = "a plain array (this value has a prototype)"; - else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) - want = "a plain array (this value has extra properties)"; - else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) - want = "a plain array (this value is frozen, sealed or not extensible)"; - } - if (!want) return true; - scr_dyn_check_fail(path, want, n); - return false; -} - -static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { - for (size_t i = 0; i < list->len; i++) - if (list->items[i].node == node) return list->items[i].capsule; - return NULL; -} - -/* `capsule` is owned by the call: pushed or released. */ -static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, - const ScrDynPath *path, unsigned depth) { - if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || - capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { - scr_dyn_release(capsule); - return true; - } - if (!scr_decl_adopt_plain(n, path)) { - scr_dyn_release(capsule); - return false; - } - if (n->rc > 1 && n->rc != SIZE_MAX) { - ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); - if (seen) { - bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; - scr_dyn_release(capsule); - if (!same) - scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); - return same; - } - scr_decl_adopt_push(list, n, capsule); - } - void *ptr = capsule->v.typed_ref.ptr; - if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { - const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; - for (size_t i = 0; i < ops->count; i++) { - if (!ops->has(ptr, i)) continue; - const ScrStr *name = ops->names[i]; - ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); - if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; - ScrDynPath p = { path, name->data, 0 }; - if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { - if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); - return false; - } - } - } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && - capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { - const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; - for (size_t i = 0; i < n->v.arr.len; i++) { - ScrDyn *child = n->v.arr.items[i]; - if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; - ScrDynPath p = { path, NULL, i }; - if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { - if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); - return false; - } - } - } - if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); - return true; -} - -/* Weak collections key a plain object by its node and a capsule by its - * native identity: move the node's entries to the identity it now has. */ -static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) { +/* ── declaration-typed boundary: in-place capsules ───────────────────── + * scr_decl_adopt.c (linked only into programs with boundary casts) turns a + * shared source node into a live capsule of the native value built from + * it. The node's old payload moves to a scratch node released here; the + * node's weak-collection entries move to the capsule's native identity. + * Nothing else in the runtime calls this. */ +static bool scr_dyn_decl_rekey(ScrDyn *node, void *ptr) { bool moved = false; if (!scr_weak_observing) return false; for (;;) { @@ -15905,11 +15780,10 @@ static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) { } } -static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) { +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { ScrDynRegion *region = scr_dyn_region_of(n); if (region) region->intact = false; - /* The old payload moves to a fresh node, which is released below. A - * misc node carries no buffers of an earlier life. */ ScrDyn *old = scr_dyn_alloc(SCR_DYN_NULL); old->kind = n->kind; old->buffer = n->buffer; @@ -15929,36 +15803,14 @@ static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) { n->symbol_properties = NULL; n->symbol_keys = NULL; memset(&n->v, 0, sizeof n->v); + n->v.typed_ref = capsule->v.typed_ref; n->v.typed_ref.ptr = capsule->v.typed_ref.retain(capsule->v.typed_ref.ptr); - n->v.typed_ref.retain = capsule->v.typed_ref.retain; - n->v.typed_ref.release = capsule->v.typed_ref.release; - n->v.typed_ref.type_key = capsule->v.typed_ref.type_key; - n->v.typed_ref.type_key_len = capsule->v.typed_ref.type_key_len; - n->v.typed_ref.materialize = capsule->v.typed_ref.materialize; - n->v.typed_ref.commit = capsule->v.typed_ref.commit; - n->v.typed_ref.array = capsule->v.typed_ref.array; - n->v.typed_ref.record = capsule->v.typed_ref.record; - n->v.typed_ref.klass = capsule->v.typed_ref.klass; - n->v.typed_ref.traced = capsule->v.typed_ref.traced; - n->v.typed_ref.observed = capsule->v.typed_ref.observed; - if (scr_decl_adopt_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; - /* A record with undeclared properties answers them from its retained - * view, as a freshly boxed capsule does. */ - if (scr_record_source_count && n->v.typed_ref.type_key_len >= 7 && - memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { + n->v.typed_ref.materialized = NULL; + n->v.typed_ref.casts = NULL; + if (scr_dyn_decl_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; + if (n->v.typed_ref.type_key_len >= 7 && memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { ScrDyn *view = scr_record_source_view(n->v.typed_ref.ptr); if (view) n->v.typed_ref.materialized = scr_dyn_retain(view); } scr_dyn_release(old); } - -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { - ScrDeclAdoptList list = { NULL, 0, 0 }; - bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); - for (size_t i = 0; i < list.len; i++) { - ScrDeclAdopt *item = &list.items[i]; - if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_decl_adopt_become(item->node, item->capsule); - scr_dyn_release(item->capsule); - } - free(list.items); -} From fe406429eb9ee980bc926bab6acff61065e967cd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:37:12 -0500 Subject: [PATCH 100/169] Keep int32 slots for long number arrays with checked integer stores A local number[] candidate whose stores are exact integers but not all proven int32s (a distance array updated with dist[v] = dist[u]! + 1), and that is only ever allocated by new Array(n).fill(v), now holds SCR_ELEM_I32 slots when n is at least 4096. A shorter fill creates doubles, which fit a typical L1 data cache. The first store of a value that is not an int32 widens the array to doubles in place (scr_arr_widen_i32). An innermost loop that accesses such arrays tests their kinds once on entry (backend/llvm/int32-kind-loops.ts). Its fast copy reads and writes them as int32 arrays; a store that would widen one leaves for a copy that re-executes it with kind tests and finishes the loop on doubles. When the arrays already hold doubles, a copy accesses them as double arrays. Other accesses test the kind each time. Only these arrays take kind tests: every other array, and every program without one, emits the same code as before. --- .../src/backend/llvm/dense-array-access.ts | 9 +- .../src/backend/llvm/emit-function.ts | 2 + .../src/backend/llvm/emit-statements.ts | 186 ++++++++++++- packages/compiler/src/backend/llvm/emitter.ts | 246 ++++++++++++++++-- .../src/backend/llvm/expr-containers.ts | 47 +++- .../src/backend/llvm/expr-primitives.ts | 86 ++++-- .../src/backend/llvm/int32-kind-loops.ts | 135 ++++++++++ .../src/backend/llvm/local-array-reads.ts | 32 ++- .../src/backend/llvm/speculative-integers.ts | 4 +- packages/compiler/src/ir/int32-arrays.ts | 66 ++++- packages/compiler/src/ir/int32-slots.ts | 25 +- packages/compiler/src/ir/ir.ts | 3 +- .../test/int32-element-array-emission.test.ts | 74 +++++- .../corpus/7327-int32-array-widening.ts.json | 6 + packages/runtime/src/scr_array.c | 30 +++ packages/runtime/src/scr_array.h | 3 + packages/runtime/test/test_array.c | 38 +++ tests/corpus/7327-int32-array-widening.ts | 186 +++++++++++++ .../7327-int32-array-widening.ts.json | 1 + 19 files changed, 1108 insertions(+), 71 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/int32-kind-loops.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json create mode 100644 tests/corpus/7327-int32-array-widening.ts create mode 100644 tests/harness/corpus-costs/7327-int32-array-widening.ts.json diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 2663c47a0..95612e57c 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -542,7 +542,9 @@ export function emitDenseArrayState( * value's reference. A dense index stores the slot, marks it present, and * extends the length when it lands at or past it; a replaced present * reference is released after the new edge is published (a release can - * collect cycles). Every other index takes the unchanged runtime call. */ + * collect cycles). Every other index takes the unchanged runtime call. + * `int32`, for an int32 array, is the value as an i32 when the caller has + * it at hand. */ export function emitDenseArraySet( host: LlvmEmitterContext, array: string, @@ -552,6 +554,7 @@ export function emitDenseArraySet( element: IrType, value: string, proven = false, + int32: string | null = null, ): void { const B = host.B; const ty = valueType(acc); @@ -566,8 +569,8 @@ export function emitDenseArraySet( } // An int32 array's stores are proven int32s (ir/int32-arrays.ts); both // dense paths below store the converted value. - let int32Value: string | null = null; - if (acc === "i32") { + let int32Value: string | null = int32; + if (acc === "i32" && int32Value === null) { int32Value = B.tmp(); B.line(`${int32Value} = fptosi double ${value} to i32`); } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 2484bc38d..b6df7bcca 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -139,6 +139,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.speculativeFunction = fn.name; emitter.speculation = null; emitter.speculationResume = null; + emitter.int32Kinds = null; + emitter.int32KindResume = null; emitter.finalFieldBorrows = !fn.async && !fn.generator && diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index c4ff602bf..c2846a9d4 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -34,9 +34,16 @@ import { dynHoleRead } from "./loop-array-borrows.js"; import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js"; import { InternalCompilerError } from "../../errors.js"; -import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js"; +import { + emitDenseArrayGet, + emitDenseArrayPop, + emitDenseArraySet, + SCR_ELEM_I32, +} from "./dense-array-access.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; -import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js"; +import { f64Lit, llvmCommentText, loadArrCount, SCR_ARR_FIELD, sizeToDouble } from "./common.js"; +import { planInt32KindLoop, type Int32KindLoopPlan } from "./int32-kind-loops.js"; +import { withinInt32 } from "../../ir/integer-ranges.js"; import { resultSlotStore } from "../result-slots.js"; import { matchIntegerArrayForLoop, @@ -411,7 +418,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const v = emitter.emitExpr(s.value); if (s.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); + if (emitter.checkedInt32Array(s.arr)) { + // An int32 array with checked stores: the int32 arm stores an int32 + // value, and widens the array for any other first. + emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); + const elem = s.arr.type.elem; + const proven = emitter.provenDenseIndex(s.arr, s.index); + emitter.emitByElementKind(arr.name, null, (acc, widened) => { + if (acc === "i32") emitter.emitCheckedInt32Store(arr.name, v.name, s.value, widened); + emitDenseArraySet(emitter, arr.name, idx, s.index, acc, elem, v.name, proven); + return null; + }); + break; + } const acc = emitter.arrayAccess(s.arr, s.arr.type.elem); + const int32 = emitter.emitInt32KindStoreCheck(s.arr, v, s.value); emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); if (acc === "ref") emitter.toHeap(v); emitDenseArraySet( @@ -423,6 +444,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.arr.type.elem, v.name, emitter.provenDenseIndex(s.arr, s.index), + int32, ); break; } @@ -478,12 +500,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.condBr(dropsLast, pop, call); B.startBlock(pop); const elem = s.arr.type.elem; - const acc = emitter.arrayAccess(s.arr, elem); - const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); - if (acc === "ref" && isRefCounted(elem)) { - const old = B.tmp(); - B.line(`${old} = inttoptr i64 ${raw} to ptr`); - emitter.releaseHeap(old, elem); + if (emitter.checkedInt32Array(s.arr)) { + emitter.emitByElementKind(arr.name, null, (acc) => { + emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); + return null; + }); + } else { + const acc = emitter.arrayAccess(s.arr, elem); + const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call); + if (acc === "ref" && isRefCounted(elem)) { + const old = B.tmp(); + B.line(`${old} = inttoptr i64 ${raw} to ptr`); + emitter.releaseHeap(old, elem); + } } B.br(done); B.startBlock(call); @@ -2098,9 +2127,26 @@ export function emitLoopCopies( ) : null; if (!plan) { - const proven = allowed ? planProvenLoop(emitter, loop) : null; - if (proven) emitProvenLoop(emitter, proven, exit, emitCopy); - else emitCopy(exit); + const ordinary = (exit: string): void => { + const proven = allowed ? planProvenLoop(emitter, loop) : null; + if (proven) emitProvenLoop(emitter, proven, exit, emitCopy); + else emitCopy(exit); + }; + const kinds = + allowed && + emitter.speculativeLoopsEnabled && + emitter.int32Slots.checkedInt32Arrays.size > 0 && + emitter.int32Kinds === null && + emitter.speculation === null && + emitter.speculationResume === null + ? planInt32KindLoop( + loop, + (id) => emitter.checkedInt32Local(id), + (e) => withinInt32(emitter.integerRanges.get(e)), + ) + : null; + if (kinds) emitInt32KindLoop(emitter, kinds, exit, kinds.writes.size > 0 ? emitCopy : ordinary); + else ordinary(exit); return; } const B = emitter.B; @@ -2243,6 +2289,124 @@ export function emitLoopCopies( } } +/** An int32 kind loop (int32-kind-loops.ts). The entry test reads the kind + * of each of the plan's arrays: when every one holds int32 slots the fast + * copy runs, when every one holds doubles the wide copy, which reads and + * writes them as double arrays, and otherwise (a plan with several arrays) + * the mixed copy, which tests their kinds at each access. A failed store of + * the fast copy re-executes in the third copy, which finishes its + * iteration with kind tests and then enters the mixed copy, or the wide + * copy when the plan has one array (it just widened). Each copy is emitted + * by `ordinary`: the loop's plain copy when a store may fail, since a + * proven integer loop keeps locals in shadows of its own copy that another + * copy cannot resume from. */ +function emitInt32KindLoop( + emitter: LlEmitter, + plan: Int32KindLoopPlan, + exit: string, + ordinary: (exit: string) => void, +): void { + const B = emitter.B; + const fn = emitter.currentFunctionName; + const keys = new Set([...plan.arrays.keys()].map((id) => `${fn}\0${id}`)); + const several = keys.size > 1; + let allInt32 = "true", + allWide = "true"; + const and = (all: string, next: string): string => { + if (all === "true") return next; + const both = B.tmp(); + B.line(`${both} = and i1 ${all}, ${next}`); + return both; + }; + for (const ref of plan.arrays.values()) { + const arr = emitter.emitStableReceiver(ref, []); + const kindPtr = B.tmp(), + kind = B.tmp(), + isInt32 = B.tmp(); + B.line( + `${kindPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 ${SCR_ARR_FIELD.elem}`, + ); + emitter.markMemoryPointer(kindPtr, "array:header"); + B.line(`${kind} = load i8, ptr ${kindPtr}${emitter.fieldAliasAttachment(kindPtr)}`); + B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`); + allInt32 = and(allInt32, isInt32); + if (several) { + const isWide = B.tmp(); + B.line(`${isWide} = xor i1 ${isInt32}, true`); + allWide = and(allWide, isWide); + } + } + const fast = B.newLabel("kind.fast"), + wide = B.newLabel("kind.wide"), + mixed = several ? B.newLabel("kind.mixed") : null; + if (mixed === null) B.condBr(allInt32, fast, wide); + else { + const other = B.newLabel("kind.other"); + B.condBr(allInt32, fast, other); + B.startBlock(other); + B.condBr(allWide, wide, mixed); + } + const resume = new Map(); + for (const s of plan.writes) resume.set(s, { label: B.newLabel("kind.resume"), placed: false }); + // The header the third copy enters: the mixed copy's, or the wide copy's. + let header: string | null = null; + const capture = (label: string): void => { + header = label; + }; + B.startBlock(wide); + if (resume.size > 0 && mixed === null) emitter.speculationHeader = capture; + emitter.int32Kinds = { int32: new Set(), wide: keys, resume: new Map() }; + try { + ordinary(exit); + } finally { + emitter.speculationHeader = null; + emitter.int32Kinds = null; + } + if (mixed !== null) { + B.startBlock(mixed); + if (resume.size > 0) emitter.speculationHeader = capture; + try { + ordinary(exit); + } finally { + emitter.speculationHeader = null; + } + } + if (resume.size > 0) { + if (header === null) + throw new InternalCompilerError("llvm emitter bug: int32 kind loop copy without a header"); + // A failed store finishes its iteration here, as in a speculative + // integer loop's third copy. + B.startBlock(B.newLabel("kind.tail")); + emitter.speculationResume = resume; + emitter.speculationHeader = () => { + B.br(header!); + B.startBlock(B.newLabel("kind.unused")); + }; + try { + ordinary(exit); + } finally { + emitter.speculationResume = null; + emitter.speculationHeader = null; + } + } + B.startBlock(fast); + if (![...resume.values()].every((r) => r.placed)) { + // A write the third copy never reached: this copy tests the kinds. + ordinary(exit); + return; + } + emitter.int32Kinds = { + int32: keys, + wide: new Set(), + resume: new Map([...resume].map(([s, r]) => [s, r.label])), + }; + try { + ordinary(exit); + } finally { + emitter.int32Kinds = null; + } +} + /** At the top of a loop copy's header: lets a speculative loop find its * ordinary copy's header, and keeps its third copy out of the loop. */ function enterLoopHeader(emitter: LlEmitter, header: string): void { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 6dc35af7f..efe6097df 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -79,9 +79,18 @@ import { } from "./emit-journal.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; -import { allocatedLiteral, selectInt32Arrays } from "../../ir/int32-arrays.js"; +import { + allocatedLiteral, + fillAllocationLength, + selectInt32Arrays, +} from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; -import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js"; +import { + INT32_RANGE, + withinInt32, + type IntegerRange, + type IntegerRanges, +} from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; import { resolveSelfCaptures } from "../../ir/self-captures.js"; @@ -101,7 +110,7 @@ import { } from "./int32-abi.js"; import { type Int32Local } from "./int32-locals.js"; import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js"; -import { exactInteger, narrowInteger } from "./integer-values.js"; +import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js"; import { findInitializerBindings } from "../../ir/initializer-bindings.js"; import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js"; import { @@ -148,7 +157,12 @@ import { } from "./inline-unions.js"; import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js"; import { CheckedNarrows } from "./checked-narrows.js"; -import { emitDenseArraySet, emitDenseSlotUnlink, type ArrayAccess } from "./dense-array-access.js"; +import { + emitDenseArraySet, + emitDenseSlotUnlink, + SCR_ELEM_I32, + type ArrayAccess, +} from "./dense-array-access.js"; import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js"; import { type MapEntryFusion } from "./map-entry-fusion.js"; import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js"; @@ -169,7 +183,7 @@ import { } from "./string-slices.js"; import { LazyCaptures } from "./lazy-captures.js"; import { StackCaptures } from "./stack-captures.js"; -import { emitFieldAbsentTest, f64Lit } from "./common.js"; +import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD } from "./common.js"; import { ffiExtendsNarrowIntegers } from "../targets.js"; import { emitLiteralExpr, @@ -484,13 +498,14 @@ function versionParameters(mod: IrModule): IrModule { return versionLoopBoundParameters(mod, mangleFunction); } -/** The int32 arrays the int32 slot analysis keeps in `mod`, under the - * inputs LlEmitter gives it (selectInt32Arrays). */ +/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and + * checked), under the inputs LlEmitter gives it (selectInt32Arrays). */ function provenInt32Arrays(mod: IrModule): ReadonlySet { - return analyzeInt32Slots(mod, { + const slots = analyzeInt32Slots(mod, { dynamicStores: dynamicFieldStores(mod), initializerBindings: findInitializerBindings(mod), - }).int32Arrays; + }); + return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); } /** The emitter for `input` after the module passes every emission runs. */ @@ -738,25 +753,42 @@ export class LlEmitter { /** The fill temporaries (`new Array(n).fill(v)`) of those allocations: * the same arrays, so int32 too. */ int32ArrayTemps = new Set(); + /** The same for the current function's int32 arrays with checked stores, + * which are only allocated by fills (ir/int32-arrays.ts + * checkableInt32Allocations): their temporaries' literals, with the fill's + * length (a literal constructs SCR_ELEM_I32 storage when that length is at + * least CHECKED_INT32_MIN_LENGTH, SCR_ELEM_F64 otherwise), and their + * temporaries. */ + checkedAllocations = new Map(); + checkedArrayTemps = new Set(); + /** The fill calls (`.fill(v)`) of those allocations: a value that is not + * an int32 widens the new array first. */ + checkedFills = new Set(); /** Collect the int32 allocations of `fn`, about to be emitted. */ findInt32Allocations(fn: IrFunction): void { this.int32Allocations.clear(); this.int32ArrayTemps.clear(); - if (this.int32Slots.int32Arrays.size === 0) return; + this.checkedAllocations.clear(); + this.checkedArrayTemps.clear(); + this.checkedFills.clear(); + const { int32Arrays, checkedInt32Arrays } = this.int32Slots; + if (int32Arrays.size === 0 && checkedInt32Arrays.size === 0) return; everyStmtList(fn.body, { stmt: (s) => { - if ( - (s.kind === "varDecl" || s.kind === "assign") && - this.int32Slots.int32Arrays.has(`${fn.name}\0${s.localId}`) - ) { - const value = s.kind === "varDecl" ? s.init : s.value; - const allocation = value && allocatedLiteral(value); - if (allocation) { - this.int32Allocations.add(allocation.literal); - if (allocation.temporary !== null) this.int32ArrayTemps.add(allocation.temporary); - } - } + if (s.kind !== "varDecl" && s.kind !== "assign") return true; + const key = `${fn.name}\0${s.localId}`; + const checked = checkedInt32Arrays.has(key); + if (!checked && !int32Arrays.has(key)) return true; + const value = s.kind === "varDecl" ? s.init : s.value; + const allocation = value && allocatedLiteral(value); + if (!allocation) return true; + const length = checked ? fillAllocationLength(value) : null; + if (length !== null) this.checkedAllocations.set(allocation.literal, length); + else if (!checked) this.int32Allocations.add(allocation.literal); + if (allocation.temporary !== null) + (checked ? this.checkedArrayTemps : this.int32ArrayTemps).add(allocation.temporary); + if (checked && value !== allocation.literal) this.checkedFills.add(value!); return true; }, expr: () => true, @@ -766,16 +798,174 @@ export class LlEmitter { /** Whether `arr` is one of the current function's int32 arrays * (ir/int32-arrays.ts, kept by the int32 slot analysis). */ int32Array(arr: IrExpr): boolean { - if (arr.kind !== "varRef" || this.int32Slots.int32Arrays.size === 0) return false; + if (arr.kind !== "varRef") return false; + if (this.int32KindArray(arr)) return true; + if (this.int32Slots.int32Arrays.size === 0) return false; return ( this.int32ArrayTemps.has(arr.localId) || this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`) ); } + /** Whether `arr` is one of the current function's int32 arrays with + * checked stores: int32 slots until a store widens it to doubles. Every + * access tests its kind (emitByElementKind), except in the fast copy of + * an int32 kind loop, which reads and writes it as an int32 array. */ + checkedInt32Array(arr: IrExpr): boolean { + if (arr.kind !== "varRef" || this.int32KindArray(arr)) return false; + if (this.int32Kinds?.wide.has(`${this.currentFunctionName}\0${arr.localId}`)) return false; + return this.checkedInt32Local(arr.localId); + } + + /** Whether the local `localId` holds one of the current function's int32 + * arrays with checked stores. */ + checkedInt32Local(localId: string): boolean { + if (this.int32Slots.checkedInt32Arrays.size === 0) return false; + return ( + this.checkedArrayTemps.has(localId) || + this.int32Slots.checkedInt32Arrays.has(`${this.currentFunctionName}\0${localId}`) + ); + } + + /** Whether `arr` is an int32 array with checked stores that the fast copy + * of the int32 kind loop being emitted holds as an int32 array. */ + int32KindArray(arr: IrExpr): boolean { + return ( + this.int32Kinds !== null && + arr.kind === "varRef" && + this.int32Kinds.int32.has(`${this.currentFunctionName}\0${arr.localId}`) + ); + } + + /** Before a store of `value` (of `expr`) into `arr`, in the fast copy of + * an int32 kind loop: continue when it is an int32 that is not -0, and + * otherwise leave for the statement's resume point, where it re-executes + * with kind tests. Returns the value as an i32 when it is at hand as an + * integer, or null when the store converts the double itself. */ + emitInt32KindStoreCheck(arr: IrExpr, value: LlValue, expr: IrExpr): string | null { + if (!this.int32KindArray(arr) || withinInt32(this.integerRanges.get(expr))) return null; + const integer = exactInteger(this, value, expr); + if (integer !== null && withinInt32(integer.range)) return narrowInteger(this, integer); + const resume = this.int32KindResume; + if (resume === null) + throw new InternalCompilerError("llvm emitter bug: an int32 kind store without a resume"); + const B = this.B; + const ok = B.tmp(), + int = B.tmp(); + if (integer !== null) { + // An exact integer: a range test on its 64-bit form. + const wide = widenInteger(this, integer), + low = B.tmp(), + high = B.tmp(); + B.line(`${low} = icmp sge i64 ${wide}, -2147483648`); + B.line(`${high} = icmp sle i64 ${wide}, 2147483647`); + B.line(`${ok} = and i1 ${low}, ${high}`); + const store = B.newLabel("arr.store.i32"); + B.condBr(ok, store, resume); + B.startBlock(store); + B.line(`${int} = trunc i64 ${wide} to i32`); + return int; + } + this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)"); + const back = B.tmp(), + exact = B.tmp(), + bits = B.tmp(), + notNegativeZero = B.tmp(); + B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value.name})`); + B.line(`${back} = sitofp i32 ${int} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${value.name}`); + B.line(`${bits} = bitcast double ${value.name} to i64`); + B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`); + B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`); + const store = B.newLabel("arr.store.i32"); + B.condBr(ok, store, resume); + B.startBlock(store); + return int; + } + + /** Emit an access to the checked int32 array `array` once per element + * kind: `arm("i32", widened)` while it holds int32 slots and + * `arm("f64", widened)` after it widened, joined after both. `widened` is + * the label of the double arm, where an int32 arm's store of a value that + * is not an int32 continues after widening the array. Returns the phi of + * the arms' results (of LLVM type `ty`), or null when `ty` is null. */ + emitByElementKind( + array: string, + ty: string | null, + arm: (acc: "i32" | "f64", widened: string) => string | null, + ): string | null { + const B = this.B; + const narrow = B.newLabel("arr.kind.i32"), + wide = B.newLabel("arr.kind.f64"), + join = B.newLabel("arr.kind.join"); + const kindPtr = B.tmp(), + kind = B.tmp(), + isInt32 = B.tmp(); + B.line( + `${kindPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.elem}`, + ); + this.markMemoryPointer(kindPtr, "array:header"); + B.line(`${kind} = load i8, ptr ${kindPtr}${this.fieldAliasAttachment(kindPtr)}`); + B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`); + B.condBr(isInt32, narrow, wide); + B.startBlock(narrow); + const narrowValue = arm("i32", wide); + const narrowEnd = B.isTerminated() ? null : B.currentLabel(); + if (narrowEnd !== null) B.br(join); + B.startBlock(wide); + const wideValue = arm("f64", wide); + const wideEnd = B.isTerminated() ? null : B.currentLabel(); + if (wideEnd !== null) B.br(join); + B.startBlock(join); + // Arms that end in their own branches (a read's shared join) leave this + // join without predecessors. + if (narrowEnd === null && wideEnd === null) B.terminate("unreachable"); + if (ty === null) return null; + const arms: string[] = []; + if (narrowEnd !== null) arms.push(`[ ${narrowValue}, %${narrowEnd} ]`); + if (wideEnd !== null) arms.push(`[ ${wideValue}, %${wideEnd} ]`); + const result = B.tmp(); + B.line(`${result} = phi ${ty} ${arms.join(", ")}`); + return result; + } + + /** Before storing the double `value` (of `expr`) into an int32 arm + * (emitByElementKind) of the checked int32 array `array`: continue in this + * arm when it is an int32 that is not -0, or widen the array and continue + * at `widened`. A value proven an int32 needs no test. */ + emitCheckedInt32Store(array: string, value: string, expr: IrExpr, widened: string): void { + if (withinInt32(this.integerRanges.get(expr))) return; + const B = this.B; + this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)"); + this.declare("declare void @scr_arr_widen_i32(ptr)"); + const int = B.tmp(), + back = B.tmp(), + exact = B.tmp(), + bits = B.tmp(), + notNegativeZero = B.tmp(), + ok = B.tmp(); + B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value})`); + B.line(`${back} = sitofp i32 ${int} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${value}`); + B.line(`${bits} = bitcast double ${value} to i64`); + B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`); + B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`); + const store = B.newLabel("arr.store.i32"), + widen = B.newLabel("arr.widen"); + B.condBr(ok, store, widen); + B.startBlock(widen); + B.line(`call void @scr_arr_widen_i32(ptr ${array})`); + B.br(widened); + B.startBlock(store); + } + /** The access an element operation on `arr` (of element type `elem`) * takes: int32 arrays read and write their 4-byte slots. */ arrayAccess(arr: IrExpr, elem: IrType): ArrayAccess { + if (this.checkedInt32Array(arr)) + throw new InternalCompilerError( + "llvm emitter bug: a checked int32 array without its kind test", + ); return this.int32Array(arr) ? "i32" : elemAccess(elem); } @@ -1225,6 +1415,14 @@ export class LlEmitter { * (`enterLoopHeader`): records the ordinary copy's header, or sends the * third copy's header there. */ speculationHeader: ((header: string) => void) | null = null; + /** A copy of an int32 kind loop (int32-kind-loops.ts) being emitted: the + * int32 arrays with checked stores it reads and writes as int32 arrays + * (the fast copy) or as double arrays (an ordinary copy entered only + * after they widened), as `function\0local` keys, and where each of the + * fast copy's writes resumes in the third copy. */ + int32Kinds: { int32: Set; wide: Set; resume: Map } | null = null; + /** The resume label of the fast-copy write being emitted. */ + int32KindResume: string | null = null; /** The function being emitted, for speculative globals. */ speculativeFunction = ""; /** Every function that writes each number global (computed on demand). */ @@ -5317,10 +5515,14 @@ export class LlEmitter { const loop = s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf"; if (loop) this.loopDepth++; + // An int32 kind loop's fast-copy write leaves for its resume point. + const kindResume = this.int32KindResume; + this.int32KindResume = this.int32Kinds?.resume.get(s) ?? null; try { this.emitStmtBody(s); } finally { this.B.debugLocation = previous; + this.int32KindResume = kindResume; if (loop) this.loopDepth--; } } diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8c4279846..1ba5da818 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -615,9 +615,12 @@ export function emitArrIntrinsic( throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array"); const elem = e.receiver.type.elem; // Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others - // take the runtime family's flat access. - const access = host.arrayAccess(e.receiver, elem); - if (access === "i32" && !INT32_ARRAY_METHODS.has(e.method)) + // take the runtime family's flat access. One with checked stores tests + // its kind at each inline access (emitByElementKind) and otherwise takes + // the double entry points, which serve both kinds. + const checked = host.checkedInt32Array(e.receiver); + const access = checked ? "f64" : host.arrayAccess(e.receiver, elem); + if ((access === "i32" || checked) && !INT32_ARRAY_METHODS.has(e.method)) throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`); const acc = runtimeAccess(access); const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; @@ -640,6 +643,13 @@ export function emitArrIntrinsic( e.receiver.kind === "varRef" ? host.constantNumericTables.get(e.receiver.localId) : undefined; + if (!table && elem.kind === "f64" && checked) { + const proven = host.provenDenseIndex(e.receiver, e.args[0]!); + const value = host.emitByElementKind(r.name, "double", (kind) => + emitDenseArrayNumber(host, r.name, index, e.args[0], proven, kind), + )!; + return { name: value, type: e.type }; + } if (!table && elem.kind === "f64") return { name: emitDenseArrayNumber( @@ -685,6 +695,21 @@ export function emitArrIntrinsic( // no-op zero-argument call. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length)); + if (checked && vs.length > 0) { + // Each value appends in its own kind test: one that is not an int32 + // widens the array before it is pushed. + let length = ""; + vs.forEach((v, i) => { + length = host.emitByElementKind(r.name, "double", (kind, widened) => { + if (kind === "i32") host.emitCheckedInt32Store(r.name, v.name, e.args[i]!, widened); + return arrPush(host, r.name, kind, v.name, elem); + })!; + }); + return { name: length, type: e.type }; + } + // In an int32 kind loop's fast copy, every value is checked before + // the first appends. + vs.forEach((v, i) => host.emitInt32KindStoreCheck(e.receiver, v, e.args[i]!)); return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type }; } case "pushSpread": { @@ -759,7 +784,13 @@ export function emitArrIntrinsic( let slowLabel = ""; if (method === "pop") { slowLabel = B.newLabel("remove.slow"); - denseRaw = emitDenseArrayPop(host, r.name, access, elem, la, slowLabel); + const empty = la, + slow = slowLabel; + denseRaw = checked + ? host.emitByElementKind(r.name, "i64", (kind) => + emitDenseArrayPop(host, r.name, kind, elem, empty, slow), + )! + : emitDenseArrayPop(host, r.name, access, elem, la, slowLabel); denseLabel = B.currentLabel(); B.br(lp); B.startBlock(slowLabel); @@ -948,6 +979,14 @@ export function emitArrIntrinsic( case "fillUndefined": { const args = e.args.map((arg) => host.emitExpr(arg)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (host.checkedFills.has(e)) { + // A new int32 array with checked stores, filled with a value that + // may not be an int32: widen it first then. + const filled = B.newLabel("arr.fill.kind"); + host.emitCheckedInt32Store(r.name, args[0]!.name, e.args[0]!, filled); + B.br(filled); + B.startBlock(filled); + } const helper = e.method === "copyWithin" ? "scr_arr_copy_within" diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index f95b3adc2..1eec6a965 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -8,6 +8,7 @@ import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js"; +import { CHECKED_INT32_MIN_LENGTH } from "../../ir/int32-arrays.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { emitStringParts, numberPart, stringParts } from "./string-construction.js"; import { emitBorrowedInputs } from "./borrowed-inputs.js"; @@ -904,7 +905,30 @@ export function emitContainerExpr( const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i)); const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread)); // An int32 array's allocation (ir/int32-arrays.ts) constructs - // SCR_ELEM_I32 storage; its elements are proven int32s. + // SCR_ELEM_I32 storage; its elements are proven int32s. The empty + // temporary of a fill that allocates an int32 array with checked + // stores takes SCR_ELEM_I32 storage when the fill's length (which + // evaluates without effects) is at least CHECKED_INT32_MIN_LENGTH, and + // SCR_ELEM_F64 storage otherwise. + const fillLength = host.checkedAllocations.get(e); + if (fillLength !== undefined && e.elems.length === 0) { + const length = host.emitExpr(fillLength); + const long = B.tmp(), + kind = B.tmp(); + B.line(`${long} = fcmp oge double ${length.name}, ${f64Lit(CHECKED_INT32_MIN_LENGTH)}`); + B.line(`${kind} = select i1 ${long}, i32 ${SCR_ELEM_I32}, i32 0`); + host.declare( + small + ? `declare ptr @scr_arr_new_small(i32)` + : `declare ptr @scr_arr_new(i32, ${host.sizeType})`, + ); + B.line( + small + ? `${arr} = call ptr @scr_arr_new_small(i32 ${kind})` + : `${arr} = call ptr @scr_arr_new(i32 ${kind}, ${host.sizeType} ${capacity})`, + ); + return host.own({ name: arr, type: e.type }); + } const int32 = host.int32Allocations.has(e); if (int32) { host.declare( @@ -986,6 +1010,26 @@ export function emitContainerExpr( // Ref-element reads return +1 (the inline dense path retains like // the runtime getter); own registers the owned temp in the frame. const elem = e.arr.type.elem; + if (host.checkedInt32Array(e.arr)) { + const proven = host.provenDenseIndex(e.arr, e.index); + const value = host.emitByElementKind( + arr.name, + "double", + (kind) => + emitDenseArrayRead( + host, + arr.name, + idx, + e.index, + kind, + elem, + "scr_arr_get_f64", + true, + proven, + ).value, + )!; + return { name: value, type: e.type }; + } const acc = host.arrayAccess(e.arr, elem); const read = emitDenseArrayRead( host, @@ -1013,14 +1057,18 @@ export function emitContainerExpr( const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array"); - const t = emitDenseArrayState( - host, - arr.name, - idx, - e.index, - "has", - host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), - ); + const t = host.checkedInt32Array(e.arr) + ? host.emitByElementKind(arr.name, "i1", (kind) => + emitDenseArrayState(host, arr.name, idx, e.index, "has", slotWidth(kind)), + )! + : emitDenseArrayState( + host, + arr.name, + idx, + e.index, + "has", + host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), + ); return { name: t, type: e.type }; } case "arrayState": { @@ -1028,14 +1076,18 @@ export function emitContainerExpr( const idx = host.emitExpr(e.index); if (e.arr.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayState on non-array"); - const t = emitDenseArrayState( - host, - arr.name, - idx, - e.index, - "state", - host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), - ); + const t = host.checkedInt32Array(e.arr) + ? host.emitByElementKind(arr.name, "double", (kind) => + emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)), + )! + : emitDenseArrayState( + host, + arr.name, + idx, + e.index, + "state", + host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem), + ); return { name: t, type: e.type }; } case "arrIntrinsic": diff --git a/packages/compiler/src/backend/llvm/int32-kind-loops.ts b/packages/compiler/src/backend/llvm/int32-kind-loops.ts new file mode 100644 index 000000000..12e38c1f0 --- /dev/null +++ b/packages/compiler/src/backend/llvm/int32-kind-loops.ts @@ -0,0 +1,135 @@ +import type { IrExpr, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { MAX_LOOP_NODES, SIMPLE_STATEMENTS } from "./speculative-integers.js"; + +/** Int32 kind loops. + * + * An int32 array with checked stores (ir/int32-slots.ts) holds int32 slots + * (or doubles, when its fill was short) until a store of a value that is + * not an int32 widens it to doubles in place, so an access to it tests its + * element kind first (LlEmitter.emitByElementKind). An innermost loop that + * accesses such arrays tests their kinds once on entry instead + * (emitLoopCopies). The fast copy runs when every one of them holds int32 + * slots and reads and writes them as int32 arrays; the wide copy runs when + * every one holds doubles and accesses them as double arrays; a loop over + * several arrays whose kinds differ runs the mixed copy, which tests the + * kinds at each access. A store of the fast copy whose value is not an + * int32 branches to the same statement in a third copy, which re-executes + * it with kind tests (widening the array) and finishes the iteration, then + * enters the mixed copy, or the wide copy when the loop has one array. + * Re-execution is exact because such a store's statement evaluates nothing + * but locals, literals, number arithmetic and element reads before it + * writes. + * + * Nothing else can widen these arrays: they never reach code other than + * the compiled element operations of their own function + * (ir/int32-arrays.ts). */ +export interface Int32KindLoopPlan { + /** The arrays the fast copy reads and writes as int32 arrays, by local, + * with one of their references in the loop. */ + arrays: Map; + /** Element writes and pushes whose value may not be an int32: the + * statements the fast copy may leave at. */ + writes: Set; +} + +type KindLoop = Extract; + +/** A number or boolean expression whose evaluation has no effect: locals, + * literals, arithmetic, comparisons and element reads of local arrays. */ +function isPure(e: IrExpr): boolean { + if (e.type.kind !== "f64" && e.type.kind !== "bool") return false; + switch (e.kind) { + case "numLit": + case "boolLit": + case "varRef": + return true; + case "bin": + return ( + (e.left.type.kind === "f64" || e.left.type.kind === "bool") && + isPure(e.left) && + isPure(e.right) + ); + case "unary": + return isPure(e.operand); + case "ternary": + return isPure(e.cond) && isPure(e.then) && isPure(e.else_); + case "arrayGet": + case "arrayHas": + return e.arr.kind === "varRef" && isPure(e.index); + default: + return false; + } +} + +/** The plan for `loop`, or null. `checked` names the int32 array locals + * with checked stores of the function; `provenInt32` the values the static + * range analysis proves int32s (stores that never leave the fast copy). */ +export function planInt32KindLoop( + loop: KindLoop, + checked: (localId: string) => boolean, + provenInt32: (e: IrExpr) => boolean, +): Int32KindLoopPlan | null { + const arrays = new Map(); + const writes = new Set(); + const assigned = new Set(); + let nodes = 0; + let rejected = false; + const reject = (): false => { + rejected = true; + return false; + }; + const array = (e: IrExpr): string | null => + e.kind === "varRef" && checked(e.localId) ? e.localId : null; + const push = (e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null => + e.kind === "arrIntrinsic" && e.method === "push" && array(e.receiver) !== null ? e : null; + + const expr = (e: IrExpr): boolean => { + if (rejected) return false; + nodes++; + if (e.kind === "varRef" && checked(e.localId) && !arrays.has(e.localId)) + arrays.set(e.localId, e); + // A push the statement walk did not take as a write: its values must + // be proven int32s, since it cannot leave the fast copy. + const pushed = push(e); + if (pushed && !pushed.args.every(provenInt32)) return reject(); + return everyExprChild(e, expr, nested); + }; + // Statements inside expressions run mid-expression, where the fast copy + // cannot leave: they may not write these arrays. + const nested = (s: IrStmt): boolean => { + nodes++; + if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId); + if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) return reject(); + return everyStmtChild(s, expr, nested); + }; + const stmt = (s: IrStmt): boolean => { + if (rejected) return false; + nodes++; + if (!SIMPLE_STATEMENTS.has(s.kind)) return reject(); + if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId); + if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) { + if (!isPure(s.index) || !isPure(s.value)) return reject(); + writes.add(s); + return everyStmtChild(s, expr, stmt); + } + const pushed = s.kind === "exprStmt" ? push(s.expr) : null; + if (pushed && !pushed.args.every(provenInt32)) { + if (!pushed.args.every(isPure)) return reject(); + writes.add(s); + expr(pushed.receiver); + pushed.args.forEach(expr); + return !rejected; + } + return everyStmtChild(s, expr, stmt); + }; + + if (loop.cond) expr(loop.cond); + if (loop.kind === "for" && loop.update) stmt(loop.update); + for (const s of loop.body) stmt(s); + if (rejected || nodes > MAX_LOOP_NODES || arrays.size === 0) return null; + // A loop that rebinds one of the arrays could reach another array of + // either kind. + for (const id of arrays.keys()) if (assigned.has(id)) return null; + return { arrays, writes }; +} diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 4b7b57f6f..1c540295a 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -767,7 +767,37 @@ export function emitLocalArrayRead( } B.line(`store i32 ${read.presentTag}, ptr ${tag}`); }; - if (inline) + if (inline && scalar && host.checkedInt32Array(read.array)) + // An int32 array with checked stores: its int32 arm stores the element's + // double bits, as the double arm does. + host.emitByElementKind(array.name, null, (kind) => { + const present = + kind === "i32" + ? (value: string): void => { + const number = B.tmp(), + bits = B.tmp(); + B.line(`${number} = sitofp i32 ${value} to double`); + B.line(`${bits} = bitcast double ${number} to i64`); + storeValue(bits); + } + : storeValue; + const loadType = kind === "i32" ? "i32" : "i64"; + const induction = loopIndex !== null; + emitDenseReferenceArrayRead( + host, + array, + index, + integerIndex, + induction, + present, + no, + slow, + join, + loadType, + ); + return null; + }); + else if (inline) emitDenseReferenceArrayRead( host, array, diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts index a309a9785..00fabb788 100644 --- a/packages/compiler/src/backend/llvm/speculative-integers.ts +++ b/packages/compiler/src/backend/llvm/speculative-integers.ts @@ -35,12 +35,12 @@ export interface SpeculativeLoopPlan { type SpeculativeLoop = Extract; /** The fast copy duplicates the loop; keep the duplicated code small. */ -const MAX_LOOP_NODES = 400; +export const MAX_LOOP_NODES = 400; /** Statements whose emission is plain control flow or a single write. * Nested loops, switches and try statements keep the loop on the ordinary * path (their emission keeps state that the two copies would not share). */ -const SIMPLE_STATEMENTS = new Set([ +export const SIMPLE_STATEMENTS: ReadonlySet = new Set([ "varDecl", "assign", "exprStmt", diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index 82dcf4470..bdb3f21e2 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -3,6 +3,7 @@ import { reparseExpr } from "./serialize.js"; import { everyExprChild, everyStmtChild, + everyStmtList, transformExpr, transformStmt, transformStmtList, @@ -23,9 +24,12 @@ import { * Candidates are only proposed here. The int32 slot analysis * (int32-slots.ts) keeps the ones whose every stored element is a proven * int32 (never -0), assuming the candidates' own elements are int32s, at - * a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. Their - * element-reader helper calls are inlined first (rewriteCandidates), so - * no shared helper ever receives an int32 array. */ + * a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. It keeps + * the ones whose stores are exact integers but not all proven int32s as + * int32 arrays with checked stores: SCR_ELEM_I32 until a store that is not + * an int32 widens them to doubles in place. Their element-reader helper + * calls are inlined first (rewriteCandidates), so no shared helper ever + * receives an int32 array. */ /** Array methods a candidate may be the receiver of: the runtime paths * behind them read and write int32 slots through the double-valued slot @@ -87,6 +91,62 @@ function fillAllocation(e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null { return e; } +/** Fill-form arrays shorter than this hold doubles even when they are int32 + * arrays with checked stores: their doubles fit a typical 32 KiB L1 data + * cache, so 4-byte slots save no memory traffic. */ +export const CHECKED_INT32_MIN_LENGTH = 4096; + +/** The length `n` of `new Array(n).fill(v)`, or null for any other + * allocation. */ +export function fillAllocationLength(e: IrExpr): IrExpr | null { + const fill = fillAllocation(e); + if (!fill || fill.receiver.kind !== "seqExpr") return null; + const set = fill.receiver.stmts[1]!; + return set.kind === "arraySetLength" ? set.length : null; +} + +/** A number expression whose evaluation has no effect: locals, literals and + * arithmetic. */ +function isPureLength(e: IrExpr): boolean { + if (e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + case "varRef": + return true; + case "bin": + return e.left.type.kind === "f64" && isPureLength(e.left) && isPureLength(e.right); + case "unary": + return isPureLength(e.operand); + default: + return false; + } +} + +/** Whether the candidate `id` of `fn` may hold int32 slots with checked + * stores: every array it is assigned is the fill form, with a length that + * may reach CHECKED_INT32_MIN_LENGTH and evaluates without effects (the + * backend evaluates it once more to pick the new array's kind). */ +export function checkableInt32Allocations(fn: IrFunction, id: string): boolean { + let ok = true; + everyStmtList(fn.body, { + stmt: (s) => { + if ((s.kind === "varDecl" || s.kind === "assign") && s.localId === id) { + const value = s.kind === "varDecl" ? s.init : s.value; + const length = value ? fillAllocationLength(value) : null; + if ( + length === null || + !isPureLength(length) || + (length.kind === "numLit" && length.value < CHECKED_INT32_MIN_LENGTH) + ) + ok = false; + } + return ok; + }, + expr: () => ok, + }); + return ok; +} + /** A fresh number array: a literal without spreads, or the fill form. */ export function int32ArrayAllocation(e: IrExpr): boolean { return (e.kind === "arrayLit" && (e.spreads?.length ?? 0) === 0) || fillAllocation(e) !== null; diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index bb6a06867..e6491ba80 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -10,7 +10,7 @@ import { } from "./integer-ranges.js"; import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js"; -import { int32ArrayStores } from "./int32-arrays.js"; +import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js"; import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an @@ -251,6 +251,15 @@ export class Int32Slots { * module's int32 array candidates whose every stored element the * fixpoint proved an int32. */ readonly int32Arrays = new Set(); + /** Local number arrays that may hold int32 slots with checked stores: the + * candidates whose element stores the fixpoint proved exact integers but + * not int32s, allocated only by fills that may be long. A fill of at least + * CHECKED_INT32_MIN_LENGTH elements creates one with int32 slots and a + * shorter one with doubles. Their elements give no int32 facts; compiled + * accesses test the array's kind (once per innermost loop where they can, + * backend/llvm/int32-kind-loops.ts), and a store that is not an int32 + * widens it first (scr_arr_widen_i32). */ + readonly checkedInt32Arrays = new Set(); /** Whether every present element of the number array `arr` evaluates to * (read inside `fnName`) is an int32. */ hasInt32Elements(fnName: string, arr: IrExpr): boolean { @@ -1108,8 +1117,13 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e)); // Every element stored into an int32 array candidate must be an int32 // under the facts that assume the candidates' own elements are; a - // failing store drops its array, and the function is analyzed again - // without it. + // failing store drops its array's element facts, and the function is + // analyzed again without them. When every failing store is still an + // exact integer under those facts (a counter, `a[i]! + 1`) and the array + // is only ever allocated by a fill that may be long + // (checkableInt32Allocations), it keeps int32 slots with checked stores + // (checkedInt32Arrays), which widen it to doubles in place at the first + // store that is not an int32. const arrays = arrayCandidates.get(fn.name); if (arrays) { let dropped = false; @@ -1117,7 +1131,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): const key = `l:${fn.name}\0${id}`; if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; slots.dropInt32Elements(key); - reason(`array:${fn.name}\0${id}`, "an element store is not int32"); + const checkable = (value: IrExpr): boolean => int32(value) || ranges.get(value) != null; + if (values.every(checkable) && checkableInt32Allocations(fn, id)) + slots.checkedInt32Arrays.add(`${fn.name}\0${id}`); + else reason(`array:${fn.name}\0${id}`, "an element store is not int32"); dropped = true; } if (dropped && !queued.has(fn.name)) { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 0fa873a16..fa081c594 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1076,7 +1076,8 @@ export interface IrModule { /** Local number arrays (`function\0local`) the backend may store as * int32 slots, proposed by ir/int32-arrays.ts after their element-reader * calls were inlined; the int32 slot analysis keeps the ones whose every - * stored element it proves an int32. Absent when there is none. */ + * stored element it proves an int32, and those whose stores are exact + * integers with checked stores. Absent when there is none. */ int32ArrayCandidates?: string[]; /** The WebAssembly modules translated into this program (wasm/ * translate.ts), present exactly when the program uses the WebAssembly diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts index 8e89c8562..0b68a5d3e 100644 --- a/packages/compiler/test/int32-element-array-emission.test.ts +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -63,6 +63,16 @@ function negativeZero(): number { return 1 / a[1]!; } function counter(n: number): number { + const a = new Array(n).fill(0) as number[]; + for (let i = 0; i < 1000; i++) a[i % n] = a[i % n]! + 1; + return a[0]!; +} +function shortCounter(): number { + const a = new Array(16).fill(0) as number[]; + for (let i = 0; i < 1000; i++) a[i % 16] = a[i % 16]! + 1; + return a[0]!; +} +function literalCounter(n: number): number { const a = [0]; for (let i = 0; i < n; i++) a[0] = a[0]! + 1; return a[0]!; @@ -72,7 +82,16 @@ function joined(n: number): string { return a.join(","); } const { n } = JSON.parse('{"n": 8}') as { n: number }; -console.log(compose(n | 0, 3), counts(n | 0), fraction(n), negativeZero(), counter(n), joined(n)); +console.log( + compose(n | 0, 3), + counts(n | 0), + fraction(n), + negativeZero(), + counter(n), + shortCounter(), + literalCounter(n), + joined(n), +); `; /** Every function emitted for the source function \`name\` (its int32 ABI @@ -100,15 +119,64 @@ test("arrays storing only int32 elements get 4-byte slots", async () => { expect(counts).not.toContain("popArray"); }); -test("arrays with a store that may not be an int32, or that escape, keep doubles", async () => { +test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => { const llvm = await emitLlvm(source); - for (const name of ["fraction", "negativeZero", "counter", "joined"]) { + for (const name of ["fraction", "negativeZero", "joined"]) { const fn = functions(llvm, name); expect(fn, name).not.toBe(""); expect(fn, name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/); + expect(fn, name).not.toContain("arr.kind."); } }); +test("a counter array keeps int32 slots with checked stores", async () => { + const llvm = await emitLlvm(source); + const counter = functions(llvm, "counter"); + // `a[k] = a[k]! + 1` is an exact integer but no int32 proof: a fill of at + // least 4096 elements creates SCR_ELEM_I32 storage (SCR_ELEM_F64 for a + // shorter one), accesses test the kind, and a store widens the array + // before a value that is not an int32. + expect(counter).toMatch( + /fcmp oge double %\w+, [^\n]+\n\s+%\w+ = select i1 %\w+, i32 7, i32 0\n\s+%\w+ = call ptr @scr_arr_new(_small)?\(i32 %\w+/, + ); + expect(counter).toMatch(/load i8, ptr %\w+[^\n]*\n\s+%\w+ = icmp eq i8 %\w+, 7/); + expect(counter).toContain("@llvm.fptosi.sat.i32.f64"); + expect(counter).toContain("call void @scr_arr_widen_i32("); + // Arrays with proven stores take no kind test, and a fill of a constant + // short length or an array literal keeps doubles. + for (const name of ["compose", "counts", "shortCounter", "literalCounter"]) { + const fn = functions(llvm, name); + expect(fn, name).not.toContain("arr.kind."); + expect(fn, name).not.toContain("kind.fast"); + expect(fn, name).not.toContain("scr_arr_widen_i32"); + } + for (const name of ["shortCounter", "literalCounter"]) + expect(functions(llvm, name), name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/); +}); + +test("a loop over a counter array tests its kind once on entry", async () => { + const llvm = await emitLlvm(source); + const counter = functions(llvm, "counter"); + // The loop's fast copy runs while the array holds int32 slots, with no + // kind test of its own: the sum is an integer, and a sum past the int32 + // range leaves for the third copy, which re-executes the store with kind + // tests and widens the array. + expect(counter).toMatch( + /icmp eq i8 %\w+, 7\n\s+br i1 %\w+, label %kind\.fast\d+, label %kind\.wide/, + ); + const rest = counter.slice(counter.search(/^kind\.fast\d+:/m)); + // The fast copy is the last one, before the loop's exit. + const fast = rest.slice(0, rest.search(/^loop\.e\d+:/m)); + expect(fast).not.toBe(""); + expect(fast).not.toMatch(/icmp eq i8 %\w+, 7/); + expect(fast).toMatch(/add nsw i64 %\w+, 1/); + expect(fast).toMatch( + /icmp sle i64 %\w+, 2147483647\n[^\n]*\n\s+br i1 %\w+, label %arr\.store\.i32\d+, label %kind\.resume\d+/, + ); + expect(fast).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/); + expect(counter).toMatch(/^kind\.resume\d+:/m); +}); + test("only the candidates the proof keeps are rewritten", async () => { const mod = await lowerIr(source); const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? []; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json new file mode 100644 index 000000000..050b6a9ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7327-int32-array-widening.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index bbe66e26e..b6c016e29 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -579,6 +579,36 @@ static void scr_arr_move_inline(ScrArr *a, size_t cap) { a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; } +/* Widen an int32 array (SCR_ELEM_I32) to doubles in place, one way: its + * dense values move to 8-byte slots, with their states when it has them, and + * its kind becomes SCR_ELEM_F64. Compiled code calls it before storing a + * value that is not an int32 into an int32 array with checked stores; `data` + * may move. Sparse entries already hold doubles. */ +void scr_arr_widen_i32(ScrArr *a) { + if (a->elem != SCR_ELEM_I32) return; + size_t cap = a->cap; + if (cap > 0) { + /* Int32 arrays never shift, so they have no head room; inline slots + * have no ext prefix. */ + bool inline_slots = scr_arr_inline(a); + bool states = scr_arr_has_states(a); + size_t unit = sizeof(uint64_t) + (states ? 1 : 0); + size_t prefix = inline_slots ? 0 : scr_arr_prefix(a); + if (cap > (SIZE_MAX - prefix) / unit) scr_arr_oom(); + char *start = scr_mem_alloc(prefix + cap * unit); + if (!start) scr_arr_oom(); + if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix); + uint64_t *data = (uint64_t *)(start + prefix); + const int32_t *old = (const int32_t *)a->data; + for (size_t i = 0; i < cap; i++) data[i] = scr_arr_f64_slot((double)old[i]); + if (states) memcpy((uint8_t *)data + cap * sizeof(uint64_t), scr_arr_states_end(a) - cap, cap); + if (!inline_slots && a->rc != SIZE_MAX) scr_mem_free(scr_arr_alloc_start(a)); + a->data = data; + a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS; + } + a->elem = SCR_ELEM_F64; +} + /* How a dense allocation is sized. */ typedef enum { SCR_ARR_GROW_APPEND, /* geometric headroom for further appends */ diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6034462e9..2ae366a71 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -433,6 +433,9 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap); * array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and * scr_arr_new_ops(ops, 0). Returns +1. */ ScrArr *scr_arr_new_small(ScrElemKind elem); +/* An int32 array's storage as doubles, in place (SCR_ELEM_I32 to + * SCR_ELEM_F64); no-op for any other kind. */ +void scr_arr_widen_i32(ScrArr *a); ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); /* scr_arr_new(elem, cap) for a known final length: the `cap` slots share * the array's own block (one allocation). With `states`, the array starts diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 8cbac7f57..7ca0ebd1a 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -1670,6 +1670,44 @@ static void test_i32_slots(void) { check(i32_layout_ok(k) && scr_arr_get_f64(k, 6) == 6 && scr_arr_get_f64(k, 0) == 0, "a known-count int32 array grows out of its block"); scr_arr_release(k); + + /* Widening: in place, to 8-byte double slots, keeping states. */ + ScrArr *w = scr_arr_new(SCR_ELEM_I32, 3); + for (int i = 0; i < 3; i++) scr_arr_push_f64(w, i - 1); + check(inline_slots(w), "a three-element int32 construction keeps inline slots"); + scr_arr_widen_i32(w); + check(w->elem == SCR_ELEM_F64 && !inline_slots(w) && scr_arr_len(w) == 3 && w->filled == 3 && + scr_arr_get_f64(w, 0) == -1 && scr_arr_get_f64(w, 2) == 1, + "widening moves inline int32 slots out as doubles"); + scr_arr_set_f64(w, 1, 2.5); + check(scr_arr_get_f64(w, 1) == 2.5, "a widened array stores fractions"); + scr_arr_widen_i32(w); + check(w->elem == SCR_ELEM_F64 && scr_arr_get_f64(w, 1) == 2.5, "widening twice is a no-op"); + scr_arr_release(w); + + ScrArr *h = scr_arr_new_small(SCR_ELEM_I32); + for (int i = 0; i < 100; i++) scr_arr_push_f64(h, i * 3); + scr_arr_set_len(h, 120); + scr_arr_set_undefined(h, 5); + scr_arr_set_f64(h, 4000000, -7); + size_t cap = h->cap; + scr_arr_widen_i32(h); + bool widened = h->elem == SCR_ELEM_F64 && h->cap == cap && scr_arr_len(h) == 4000001 && + scr_arr_states_end(h) == (const uint8_t *)h->data + 9 * (size_t)h->cap; + for (int i = 0; i < 100; i++) + if (i != 5) widened &= scr_arr_get_f64(h, i) == i * 3; + check(widened && scr_arr_state(h, 5) == SCR_ARR_UNDEFINED && scr_arr_state(h, 110) == SCR_ARR_HOLE && + scr_arr_get_f64(h, 4000000) == -7 && arr_sparse_len(h) == 1, + "widening keeps values, present undefined, holes and sparse entries"); + scr_arr_push_f64(h, 0.5); + check(scr_arr_get_f64(h, 4000001) == 0.5, "a widened array keeps growing as doubles"); + scr_arr_release(h); + + ScrArr *e = scr_arr_new_small(SCR_ELEM_I32); + scr_arr_widen_i32(e); + scr_arr_push_f64(e, -0.0); + check(e->elem == SCR_ELEM_F64 && signbit(scr_arr_get_f64(e, 0)), "an empty int32 array widens too"); + scr_arr_release(e); } static void test_inline_slots(void) { diff --git a/tests/corpus/7327-int32-array-widening.ts b/tests/corpus/7327-int32-array-widening.ts new file mode 100644 index 000000000..798292c11 --- /dev/null +++ b/tests/corpus/7327-int32-array-widening.ts @@ -0,0 +1,186 @@ +// Local number arrays with checked int32 stores: `new Array(n).fill(v)` +// arrays whose stores are exact integers but not proven int32s. A fill of +// at least 4096 elements creates int32 slots, and the first store past the +// int32 range widens the array to doubles in place; a shorter fill creates +// doubles. Each case runs at both sizes. Arrays with a store that has no +// exact integer range (a product of two int32s, a fraction, a value that may +// be -0), and arrays allocated as literals, keep doubles throughout. + +function counters(n: number, rounds: number): string { + const c: number[] = new Array(n).fill(0); + for (let r = 0; r < rounds; r++) + for (let i = 0; i < c.length; i++) c[i] = c[i]! + ((i & 3) + 1) * 1000000; + return `counters ${n} ${c[0]} ${c[1]} ${c[2]} ${c[3]} ${c[n - 1]} ${c.length}`; +} + +// The store at i = 45 passes 2^31 and widens the array partway through a +// loop; the remaining passes store doubles. +function denseLoop(n: number, k: number): string { + const a: number[] = new Array(n).fill(0); + for (let i = 0; i < a.length; i++) a[i] = i + k + 2147483600; + let s = 0; + for (let i = 0; i < a.length; i++) s = (s + a[i]!) % 1000003; + return `dense ${n} ${a[44]} ${a[45]} ${a[n - 1]} ${s}`; +} + +// Checked stores whose values stay int32: no widening. +function bfs(n: number): string { + const dist: number[] = new Array(n).fill(-1); + const queue: number[] = [0]; + dist[0] = 0; + for (let h = 0; h < queue.length; h++) { + const u = queue[h]!; + const edges = [(u + 1) & (n - 1), (u * 5 + 1) & (n - 1)]; + for (let j = 0; j < edges.length; j++) { + const v = edges[j]!; + if (dist[v]! < 0) { + dist[v] = dist[u]! + 1; + queue.push(v); + } + } + } + let far = 0, + sum = 0; + for (let i = 0; i < n; i++) { + if (dist[i]! > far) far = dist[i]!; + sum += dist[i]!; + } + return `bfs ${n} ${far} ${sum} ${queue.length}`; +} + +// A fill value that is not an int32 widens the new array before the fill. +function filledPastInt32(n: number, k: number): string { + const a: number[] = new Array(n).fill(k * 1000000000); + a[1] = a[1]! + 1; + const b: number[] = new Array(n).fill(k); + b[2] = b[2]! * 2; + return `filled ${n} ${a[0]} ${a[1]} ${a.length} ${b[0]} ${b[2]}`; +} + +// A chain of sums crosses 2^31 partway through; the second loop runs after +// the array widened. +function chain(n: number, step: number): string { + const a: number[] = new Array(n).fill(0); + for (let i = 1; i < n; i++) a[i] = a[i - 1]! + (i < 16 ? step : 1); + let s = 0; + for (let i = 0; i < n; i++) s = (s + (a[i]! % 1000003)) % 1000003; + for (let i = 1; i < n; i++) a[i] = a[i]! - a[i - 1]!; + return `chain ${n} ${step} ${a[1]} ${a[5]} ${a[6]} ${a[n - 1]} ${s}`; +} + +// Two arrays in one loop: one widens partway through, the other keeps +// int32 slots; then both are read in another loop. +function twoArrays(n: number, m: number, k: number): string { + const small: number[] = new Array(n).fill(1); + const big: number[] = new Array(m).fill(k); + const count = Math.min(n, m); + for (let i = 1; i < count; i++) { + small[i] = small[i - 1]! + 1; + big[i] = big[i - 1]! + (i < 8 ? 700000000 : 1); + } + let s = 0, + t = 0; + for (let i = 0; i < count; i++) { + s += small[i]!; + t = (t + (big[i]! % 997)) % 997; + } + return `two ${n} ${m} ${small[count - 1]} ${big[2]} ${big[3]} ${big[count - 1]} ${s} ${t}`; +} + +// Pushes that pass 2^31 widen the array before they append. +function pushes(n: number, k: number): string { + const a: number[] = new Array(n).fill(k); + for (let i = 0; i < 8; i++) a.push(a[a.length - 1]! + 900000000); + let s = 0; + for (let i = 0; i < a.length; i++) s = (s + (a[i]! % 10007)) % 10007; + return `pushes ${n} ${a.length} ${a[n]} ${a[n + 2]} ${a[n + 7]} ${s}`; +} + +function controlFlow(n: number, k: number): string { + const a: number[] = new Array(n).fill(k); + let stores = 0; + for (let i = 1; i < n; i++) { + if (i % 2 === 0) continue; + a[i] = a[i - 1]! + 1000000000 * (i % 4); + stores++; + if (a[i]! > 2500000000) break; + } + let i = 0; + while (i < n && a[i]! < 2000000000) i++; + let j = n; + do j--; + while (j > 0 && a[j] === k); + return `flow ${n} ${stores} ${a[1]} ${a[2]} ${a[3]} ${i} ${j}`; +} + +function readOnly(n: number, wide: boolean): string { + const a: number[] = new Array(n).fill(2); + if (wide) a[0] = a[0]! + 3000000000; + let s = 0, + present = 0; + for (let i = 0; i < n + 2; i++) { + s += a[i] ?? 0; + if (i in a) present++; + } + return `read ${n} ${wide} ${s} ${present}`; +} + +function holes(n: number): string { + // Holes, present undefined and sparse entries survive widening. + const a: number[] = new Array(n).fill(3); + a[n + 3] = a[1]! + 1; + a[n + 2] = a[n + 3]! - 1; + a[2 * n + 100000] = a[0]! + 1; + a[n + 1] = a[n + 2]! + 3000000000; + return `holes ${n} ${a.length} ${a[n]} ${n in a} ${a[n + 1]} ${a[n + 2]} ${a[2 * n + 100000]}`; +} + +// These arrays never take int32 slots: a fractional store, a product of +// two int32s, -0, an escape, and arrays allocated as literals. +function neverInt32(n: number, x: number, y: number): string[] { + const f: number[] = new Array(n).fill(1); + f[0] = f[0]! / x; + const m: number[] = new Array(n).fill(3); + m[1] = m[1]! * y; + const z: number[] = new Array(n).fill(4); + z[1] = z[1]! % 2; + z[2] = -z[0]! % 2; + const j: number[] = new Array(n).fill(3); + j[0] = j[0]! + 1; + const lit = [1, 2, 3]; + lit[0] = lit[0]! + 3000000000; + const pushed: number[] = []; + for (let i = 0; i < 4; i++) pushed.push(i * 1000000000); + pushed[0] = pushed[3]! + pushed[2]!; + return [ + `never ${f[0]} ${m[1]} ${z[1]} ${Object.is(z[2], -0)} ${j.slice(0, 2).join("|")}`, + `literals ${lit[0]} ${lit[2]} ${pushed[0]} ${pushed.length}`, + ]; +} + +const config = JSON.parse('{"small": 64, "large": 8192, "k": 3, "y": 2000000000}') as { + small: number; + large: number; + k: number; + y: number; +}; +const lines: string[] = []; +for (const n of [config.small | 0, config.large | 0]) { + lines.push(counters(n, 1000)); + lines.push(denseLoop(n, config.k | 0)); + lines.push(bfs(n)); + lines.push(filledPastInt32(n, config.k | 0)); + lines.push(chain(n, 400000000)); + lines.push(chain(n, 100000000)); + lines.push(twoArrays(n, n, config.k | 0)); + lines.push(pushes(n, config.k | 0)); + lines.push(controlFlow(n, config.k | 0)); + lines.push(readOnly(n, false)); + lines.push(readOnly(n, true)); + lines.push(holes(n)); +} +// One long array and one short: their kinds differ on entry to the loop. +lines.push(twoArrays(config.large | 0, config.small | 0, config.k | 0)); +lines.push(twoArrays(config.small | 0, config.large | 0, config.k | 0)); +lines.push(...neverInt32(config.small | 0, config.k, config.y | 0)); +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7327-int32-array-widening.ts.json b/tests/harness/corpus-costs/7327-int32-array-widening.ts.json new file mode 100644 index 000000000..ac0e8497d --- /dev/null +++ b/tests/harness/corpus-costs/7327-int32-array-widening.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 6100 } From 17826afb23604de181a3f1bcbca10041f2bed49a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:37:39 -0500 Subject: [PATCH 101/169] Renumber the new.target corpus programs to 7560 and 7561 7362 and 7363 are taken on main by other programs. The costs and the order-parity baselines move with them. --- .../tests/corpus/7362-new-target-constructors.ts.json | 6 ------ .../tests/corpus/7363-new-target-generic-registry.ts.json | 6 ------ .../tests/corpus/7560-new-target-constructors.ts.json | 6 ++++++ .../tests/corpus/7561-new-target-generic-registry.ts.json | 6 ++++++ ...rget-constructors.ts => 7560-new-target-constructors.ts} | 0 ...eric-registry.ts => 7561-new-target-generic-registry.ts} | 0 ...ructors.ts.json => 7560-new-target-constructors.ts.json} | 0 ...try.ts.json => 7561-new-target-generic-registry.ts.json} | 0 8 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json rename tests/corpus/{7362-new-target-constructors.ts => 7560-new-target-constructors.ts} (100%) rename tests/corpus/{7363-new-target-generic-registry.ts => 7561-new-target-generic-registry.ts} (100%) rename tests/harness/corpus-costs/{7362-new-target-constructors.ts.json => 7560-new-target-constructors.ts.json} (100%) rename tests/harness/corpus-costs/{7363-new-target-generic-registry.ts.json => 7561-new-target-generic-registry.ts.json} (100%) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json deleted file mode 100644 index f38c901e0..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7362-new-target-constructors.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7362-new-target-constructors.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json deleted file mode 100644 index a1c8cb535..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7363-new-target-generic-registry.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7363-new-target-generic-registry.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json new file mode 100644 index 000000000..b4426af2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7560-new-target-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json new file mode 100644 index 000000000..dc4eab819 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7561-new-target-generic-registry.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7362-new-target-constructors.ts b/tests/corpus/7560-new-target-constructors.ts similarity index 100% rename from tests/corpus/7362-new-target-constructors.ts rename to tests/corpus/7560-new-target-constructors.ts diff --git a/tests/corpus/7363-new-target-generic-registry.ts b/tests/corpus/7561-new-target-generic-registry.ts similarity index 100% rename from tests/corpus/7363-new-target-generic-registry.ts rename to tests/corpus/7561-new-target-generic-registry.ts diff --git a/tests/harness/corpus-costs/7362-new-target-constructors.ts.json b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json similarity index 100% rename from tests/harness/corpus-costs/7362-new-target-constructors.ts.json rename to tests/harness/corpus-costs/7560-new-target-constructors.ts.json diff --git a/tests/harness/corpus-costs/7363-new-target-generic-registry.ts.json b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json similarity index 100% rename from tests/harness/corpus-costs/7363-new-target-generic-registry.ts.json rename to tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json From e6659132006c5198903c30910ca03838e797e9fa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:39:37 -0500 Subject: [PATCH 102/169] Print accessor properties of arrays and fence number keys in defineProperty - An enumerable getter or setter on an array printed as undefined: the property loop rendered the accessor's unused data slot. It now prints [Getter], [Setter] or [Getter/Setter], as util.inspect does. - Object.defineProperty on a live native array with a number key that is not an array index (-3, 1.5) skipped the numeric-property fence and lost the property on commit. A number key now takes the same fence as its string spelling. --- packages/runtime/src/scr_inspect.c | 11 +++++++++- packages/runtime/src/scr_json.c | 13 ++++++++---- tests/corpus/7062-inspect-array-properties.ts | 7 +++++++ tests/harness/errors.test.ts | 21 +++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index bca2b88fe..1dce8d3c2 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -992,6 +992,15 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) { /* Property-name rendering: bare when the key matches Node's keyStrRegExp * (/^[a-zA-Z_][a-zA-Z_0-9]*$/, '__proto__' excepted), quoted otherwise. */ +/* util.inspect's text for an accessor property: [Getter], [Setter] or + * [Getter/Setter], by which of the two are functions. +1. */ +static ScrStr *insp_accessor_tag(const ScrDynEntry *ent) { + bool get = ent->getter && ent->getter->kind == SCR_DYN_FUNC; + bool set = ent->setter && ent->setter->kind == SCR_DYN_FUNC; + const char *text = get && set ? "[Getter/Setter]" : get ? "[Getter]" : set ? "[Setter]" : "undefined"; + return scr_str_new(text, strlen(text)); +} + static void insp_key_into(InspBuf *b, const char *key, size_t len) { if (len == 9 && memcmp(key, "__proto__", 9) == 0) { ib_cstr(b, "['__proto__']"); @@ -1539,7 +1548,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { for (size_t k = 0; props && k < props->v.obj.len; k++) { const ScrDynEntry *ent = &props->v.obj.entries[k]; if (!ent->enumerable) continue; - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; insp_key_into(&eb, ent->key, ent->key_len); ib_cstr(&eb, ": "); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 46d3495ff..e3470854e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6759,11 +6759,16 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { if (scr_dyn_error_prototype_fence(target)) return NULL; - if (target->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(target) && key->kind == SCR_DYN_STR) { + if (target->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(target) && + (key->kind == SCR_DYN_STR || key->kind == SCR_DYN_NUM)) { /* A native array's numeric properties ("-1", "1.5") live in its - * property table, which holds values without attributes. */ - double number; - if (scr_arr_numeric_key(key->v.str->data, key->v.str->len, &number)) { + * property table, which holds values without attributes. A number key + * names one unless it is an array index. */ + double number = key->kind == SCR_DYN_NUM ? key->v.num : 0; + bool numeric = key->kind == SCR_DYN_NUM + ? !(number >= 0 && number < 4294967295.0 && number == trunc(number)) + : scr_arr_numeric_key(key->v.str->data, key->v.str->len, &number); + if (numeric) { static const char message[] = "Object.defineProperty on a numeric property of a native array is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); return NULL; diff --git a/tests/corpus/7062-inspect-array-properties.ts b/tests/corpus/7062-inspect-array-properties.ts index aa18cfca9..7cd5ca984 100644 --- a/tests/corpus/7062-inspect-array-properties.ts +++ b/tests/corpus/7062-inspect-array-properties.ts @@ -52,3 +52,10 @@ console.log(parsed); const plain: number[] = [1, 2, 3]; console.log(plain, [] as number[]); + +const accessed: number[] = [1, 2]; +const accessedBag = accessed as unknown as Bag; +Object.defineProperty(accessedBag, "both", { get: () => 1, set: (_v: unknown) => {}, enumerable: true }); +Object.defineProperty(accessedBag, "getter", { get: () => 2, enumerable: true }); +Object.defineProperty(accessedBag, "setter", { set: (_v: unknown) => {}, enumerable: true }); +console.log(accessed); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 6afa1780d..5a1c3a336 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -2006,4 +2006,25 @@ console.log("unreachable"); /^Uncaught Error: Object\.defineProperty on a numeric property of a native array is not supported yet\n$/, ); }); + + test("a number key that is not an array index takes the same fence", async () => { + const r = await compileAndRun( + "array-numeric-define-property-number", + `const words: string[] = ["a"]; +for (const key of [-3, 1.5]) { + try { + Object.defineProperty(words as unknown as Record, key, { value: "kept" }); + } catch (e) { + console.log((e as Error).message); + } +} +console.log(words.length); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Object.defineProperty on a numeric property of a native array is not supported yet\n".repeat(2) + + "1\n", + ); + }); }); From df9f83bbc1b863f400b9ced701fcf7f7398cbd69 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:47:37 -0500 Subject: [PATCH 103/169] Refuse new.target over function-local subclasses lowered later Lowering collects a function-local class with the body declaring it, so a local subclass whose function lowers after its base's constructor was missing from new.target's dispatch, which then answered with the base for its instances. The refusal no longer depends on that order: the program's function-local classes are found once, and one that extends the constructor's class, directly or through other classes its extends clauses name, refuses new.target as a collected one does. --- .../src/frontend/lowering/lower-classes.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 3 + .../src/frontend/lowering/new-target.ts | 96 ++++++++++++++++++- .../new-target-late-local-subclass.ts.json | 6 ++ .../new-target-late-local-subclass.ts | 15 +++ .../new-target-late-local-subclass.ts.txt | 6 ++ 6 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json create mode 100644 tests/diagnostics/new-target-late-local-subclass.ts create mode 100644 tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b23697cab..1b8941e3e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -238,7 +238,7 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { return valid; } -function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { +export function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 93c6246d9..9d7c83810 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2184,6 +2184,9 @@ export class Lowerer { * and erased instances). */ erasingDepth = 0; localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = []; + /** The function-local classes of the program with an `extends` clause, whether lowering collected + * them yet or not (new-target.ts); found on first use. */ + functionLocalSubclasses: ts.ClassLikeDeclaration[] | undefined = undefined; /** Instance types (by checker type id) of the local classes generic * factory calls return, and the per-instance class each names. */ readonly localClassInstanceTypes = new Map(); diff --git a/packages/compiler/src/frontend/lowering/new-target.ts b/packages/compiler/src/frontend/lowering/new-target.ts index 4cf271469..83994ca6f 100644 --- a/packages/compiler/src/frontend/lowering/new-target.ts +++ b/packages/compiler/src/frontend/lowering/new-target.ts @@ -16,7 +16,7 @@ import { varRef } from "../../ir/build.js"; import { locOf } from "../program.js"; import type { Lowerer } from "./lowerer.js"; import type { ClassInfo } from "./lower-classes.js"; -import { classValueRef } from "./lower-classes.js"; +import { classValueRef, functionLocalClass } from "./lower-classes.js"; export function isNewTarget(node: ts.Node): node is ts.MetaProperty { return ( @@ -56,6 +56,90 @@ function depth(info: ClassInfo): number { return d; } +/** The function-local classes of the program that extend a class. */ +function functionLocalSubclasses(lowerer: Lowerer): ts.ClassLikeDeclaration[] { + let found = lowerer.functionLocalSubclasses; + if (found === undefined) { + const classes: ts.ClassLikeDeclaration[] = []; + for (const sf of lowerer.moduleOrder) + ts.walkPreorder(sf, (node) => { + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + baseExpressionOf(node) !== undefined && + functionLocalClass(node) + ) + classes.push(node); + }); + lowerer.functionLocalSubclasses = found = classes; + } + return found; +} + +function baseExpressionOf(node: ts.ClassLikeDeclaration): ts.Expression | undefined { + let base = node.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + while (base !== undefined && ts.isParenthesizedExpression(base)) base = base.expression; + return base; +} + +/** Whether the class `node` extends the class of `info` (for an + * instantiation, its family), directly or through other classes, as far as + * its `extends` clauses name classes. */ +function derivesFrom( + lowerer: Lowerer, + node: ts.ClassLikeDeclaration, + info: ClassInfo, + seen: Set, +): boolean { + if (seen.has(node)) return false; + seen.add(node); + const base = baseExpressionOf(node); + const name = + base === undefined + ? undefined + : ts.isIdentifier(base) + ? base + : ts.isPropertyAccessExpression(base) + ? base.name + : undefined; + let symbol = name === undefined ? undefined : lowerer.checker.getSymbolAtLocation(name); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + if (symbol === undefined) return false; + const named = lowerer.classBySymbol.get(symbol); + if (named !== undefined) + return ( + named === info || + named === info.genericInstance?.family || + lowerer.isSubclassOf(named.def.name, info.def.name) + ); + // A class lowering has not collected: through its declaration. + for (const declaration of symbol.declarations) { + let cls: ts.Node | undefined = declaration; + if (ts.isVariableDeclaration(cls)) { + cls = cls.initializer; + while (cls !== undefined && ts.isParenthesizedExpression(cls)) cls = cls.expression; + } + if ( + cls !== undefined && + (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && + derivesFrom(lowerer, cls, info, seen) + ) + return true; + } + return false; +} + +/** `node`'s name, quoted, for diagnostics. */ +function localClassName(node: ts.ClassLikeDeclaration): string { + if (node.name) return `'${node.name.text}'`; + const parent = node.parent; + return parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) + ? `'${parent.name.text}'` + : "a class expression"; +} + /** The classes `new.target` can denote in the constructor being lowered, * or null after a diagnostic. */ function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { @@ -98,6 +182,16 @@ function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { return null; } } + // Lowering collects a function-local class with the body declaring it, + // which may come after this constructor. + for (const local of functionLocalSubclasses(lowerer)) { + if (!derivesFrom(lowerer, local, info, new Set())) continue; + lowerer.unsupported( + "SC1090", + expr, + `'new.target' where ${localClassName(local)} is a function-local class (each evaluation creates a new class)`, + ); + } let self: IrExpr | null = null; if (classes.length > 1) { const thisLocal = lowerer.resolveThis(expr); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json new file mode 100644 index 000000000..7046e28d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-late-local-subclass.ts" + ], + "diags": [] +} diff --git a/tests/diagnostics/new-target-late-local-subclass.ts b/tests/diagnostics/new-target-late-local-subclass.ts new file mode 100644 index 000000000..658a7148e --- /dev/null +++ b/tests/diagnostics/new-target-late-local-subclass.ts @@ -0,0 +1,15 @@ +// A function-local subclass refuses new.target in its base's constructor +// also when lowering collects it after that constructor: make() is lowered +// after the top-level code that constructs Base. +class Base { + readonly label: string; + constructor() { + this.label = new.target.name; + } +} +console.log(new Base().label); +function make(): Base { + const Local = class extends Base {}; + return new Local(); +} +console.log(make().label); diff --git a/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt new file mode 100644 index 000000000..feb663813 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-late-local-subclass.ts:7:18 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet + + 6 | constructor() { + 7 | this.label = new.target.name; + | ^~~~~~~~~~ + 8 | } \ No newline at end of file From ee79ce16e3880c4e23a1895dd29fa586ad15aa0a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:49:03 -0500 Subject: [PATCH 104/169] Give the zod3-infer npm case the long per-case timeout A sanitized build of zod3-infer compiles zod 3's types module, as zod3's does, and takes about as long as the default 120 s on a CI runner. --- tests/harness/npm.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 13ad3f438..9a13c0968 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -112,10 +112,15 @@ const KNOWN: Record = { const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); /** Per-case timeouts past the default 120 s: the undici cases compile the - * built-in undici implementation's module graph, and zod3 compiles zod 3's - * 3,700-line types module. A sanitized build of either takes minutes on a - * CI runner. */ -const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000, zod3: 600_000 }; + * built-in undici implementation's module graph, and zod3 and zod3-infer + * compile zod 3's 3,700-line types module. A sanitized build of either + * takes minutes on a CI runner. */ +const TIMEOUTS: Record = { + undici5: 600_000, + undici7: 600_000, + zod3: 600_000, + "zod3-infer": 600_000, +}; interface RunResult { stdout: Buffer; From 12166956b8e1d3e6fc752be5585832fb72ab700e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:57:29 -0500 Subject: [PATCH 105/169] Spread protocol iterables into Math.min and Math.max Math.min(...src) and Math.max(...src) over an untyped source now take its values the way a spread does: arrays, strings, bytes and collections pack, a native iterator (a Map's keys()) drains, and other iterables (a generator, an object with a Symbol.iterator method) step their iterator to the end. Before, only the packing kinds were accepted and every other iterable threw the spread TypeError. --- packages/runtime/src/scr_json.c | 11 +++++++- tests/corpus/7488-math-spread-untyped.js | 36 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 9abb7796b..c6ff65d59 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1557,6 +1557,8 @@ static ScrDyn *scr_dyn_not_iterable(const ScrDyn *src, const ScrStr *msg) { return NULL; } +static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); + /* Math.max(...src) / Math.min(...src) over an untyped iterable: every * value's ToNumber runs in order, then NaN poisons and ±0 orders as in * Node. A non-iterable source throws V8's call-spread TypeError: `what` @@ -1575,7 +1577,14 @@ double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what) { } static const char spread_msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function"; ScrStr *msg = scr_str_new(spread_msg, sizeof spread_msg - 1); - ScrDyn *pack = scr_dyn_iter_pack(src, msg); + /* The spread takes every value before the call: arrays, strings, bytes + * and collections pack, a native iterator (a Map's keys()) drains, and + * other iterables (a generator, an object with a Symbol.iterator method) + * step their iterator to the end. */ + ScrDyn *pack = src->kind == SCR_DYN_HANDLE && (src->v.handle.tag == SCR_DYNH_ITERATOR || + scr_dyn_handle_ops_of(src)->iter_step) + ? scr_native_iterator_drain(src) + : scr_dyn_destructure_rest(src, msg); scr_str_release(msg); if (!pack) return NAN; double best = max ? -INFINITY : INFINITY; diff --git a/tests/corpus/7488-math-spread-untyped.js b/tests/corpus/7488-math-spread-untyped.js index a30ece320..028135d41 100644 --- a/tests/corpus/7488-math-spread-untyped.js +++ b/tests/corpus/7488-math-spread-untyped.js @@ -31,3 +31,39 @@ for (const k of Object.keys(sources)) { console.log(e.constructor.name, e.message); } } + +// Iterables that step through the iterator protocol, reached through an +// untyped lookup: a generator, an object with a Symbol.iterator method, a +// Set, a Map's keys() and an iterator that ends early. +function* generate(values) { + for (const value of values) yield value; +} +const makers = { + generator: () => generate([4, "1", 7]), + custom: () => ({ + *[Symbol.iterator]() { + yield 10; + yield -2; + }, + }), + set: () => new Set([3, 8, 3]), + keys: () => new Map([[2, "a"], [6, "b"]]).keys(), + empty: () => generate([]), +}; +for (const k of Object.keys(makers)) { + console.log(k, Math.min(...makers[k]()), Math.max(...makers[k]())); +} +const stepped = []; +const counting = { + [Symbol.iterator]() { + let n = 0; + return { + next() { + stepped.push(n); + return n < 3 ? { value: n++, done: false } : { value: undefined, done: true }; + }, + }; + }, +}; +const lookup = { counting }; +console.log(Math.max(...lookup.counting), stepped.join(",")); From ca2d5bd9c726b85350b024e7687e5611b12f428b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:58:24 -0700 Subject: [PATCH 106/169] Support String.prototype.at on strings held in untyped values Hono's mergePath reads base?.at(-1) on an untyped parameter. The checked-dynamic method dispatch answered at() on strings with a not-supported error. It now returns the UTF-16 code unit at the index, counting from the end for a negative index, and undefined outside the string, as String.prototype.at does. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_dyn_invoke.c | 15 ++++++++++++++- tests/corpus/7403-dyn-string-at.mjs | 9 +++++++++ .../corpus-costs/7403-dyn-string-at.mjs.json | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/7403-dyn-string-at.mjs create mode 100644 tests/harness/corpus-costs/7403-dyn-string-at.mjs.json diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 14d11dc67..a32689811 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -972,7 +972,20 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || + if (dyn_name_is(method, "at")) { + /* String.prototype.at: one UTF-16 code unit, counted from the end + * for a negative index; undefined outside the string. */ + double index = dyn_index_arg(args, argc, 0, 0, what); + if (scr_exc_pending()) return NULL; + double len = (double)scr_str_utf16_len(s); + double k = index < 0 ? len + index : index; + if (k < 0 || k >= len) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *unit = scr_str_slice(s, k, k + 1); + ScrDyn *r = scr_dyn_new_str(unit); /* retains */ + scr_str_release(unit); + return r; + } + if (dyn_name_is(method, "concat") || dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || diff --git a/tests/corpus/7403-dyn-string-at.mjs b/tests/corpus/7403-dyn-string-at.mjs new file mode 100644 index 000000000..68e865000 --- /dev/null +++ b/tests/corpus/7403-dyn-string-at.mjs @@ -0,0 +1,9 @@ +// String.prototype.at on strings held in untyped (checked-dynamic) values, +// as Hono's mergePath reads `base?.at(-1)`. +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}`}`; +}; +console.log(mergePath("/api", "/users"), mergePath("api/", "/"), mergePath("/a", "b", "/c")); +const values = JSON.parse(`["hello", "", "a\\ud83d\\ude00b"]`); +for (const v of values) console.log(JSON.stringify([v.at(0), v.at(-1), v.at(1), v.at(99), v.at(-99), v.at(1.7), v.at()])); diff --git a/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json b/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json new file mode 100644 index 000000000..cd29a3461 --- /dev/null +++ b/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 15000 } From 77c063682675520bb5804eb27db6e48bcdc71b80 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:00:01 -0500 Subject: [PATCH 107/169] Include star re-exports in namespace objects, and read causes and instanceof Error like Node under a runtime Error parent - A module namespace converted to an untyped object, and a computed namespace read, use the module's resolved exports, which include the members of `export *` declarations. - `super(message, options)` through a runtime Error parent installs the cause as Error does: an inherited `cause` counts, the value is read through its getter, and the getter's exception propagates. - `value instanceof Error` on an instance of a class whose heritage is evaluated at run time tests that class's prototype chain for Error.prototype. A program with such a class checks prototype chains for `instanceof Error` on untyped values. - The cache of names written on the global object is a Lowerer field instead of a WeakMap keyed by the Lowerer. - errors.test.ts: `"prototype" in Map` answers from the builtin constructor's keys now, so it leaves the refused reads and joins a check of `in` probes against Node's answers. --- .../lowering/class-dynamic-dispatch.ts | 81 +++++++++++++++++++ .../lowering/class-method-mutations.ts | 26 ++++++ .../src/frontend/lowering/dispatch-sites.ts | 1 + .../lowering/expressions/property-access.ts | 6 +- .../compiler/src/frontend/lowering/lowerer.ts | 9 ++- .../lowering/module-namespace-elements.ts | 21 ++--- packages/compiler/src/frontend/ts7/checker.ts | 8 ++ .../main.js.json | 1 + packages/runtime/src/scr_json.c | 13 ++- ...481-class-runtime-heritage-object-error.js | 27 +++++++ .../7487-namespace-spread-untyped/all.js | 2 + .../7487-namespace-spread-untyped/main.js | 5 ++ tests/harness/errors.test.ts | 9 ++- 13 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 tests/corpus/7487-namespace-spread-untyped/all.js diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 8a4a1e417..e394718d1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -908,6 +908,11 @@ export class ClassDynamicDispatch { private readonly prototypeClasses = new Set(); private prototypeIdentityHelper: IrFunction | null = null; private readonly prototypeIdentityClasses = new Set(); + /** `value instanceof Error` through Error.prototype: boxed instances of + * classes with a heritage evaluated at run time test their prototype + * data's chain. */ + private errorPrototypeHelper: IrFunction | null = null; + private readonly errorPrototypeClasses = new Set(); private readonly instancePrototypes = new Map }>(); private readonly checkedCasts = new Map }>(); /** The for-in key helpers, by libCall (dyn and native string[] keys). */ @@ -1114,6 +1119,49 @@ export class ClassDynamicDispatch { }); changed = true; } + if ( + this.errorPrototypeHelper && + !this.errorPrototypeClasses.has(className) && + hasRuntimeHeritage(info) + ) { + this.errorPrototypeClasses.add(className); + const loc = this.errorPrototypeHelper.loc; + const type: IrType = { kind: "object", className }; + const value = varRef("value", DYN, loc); + const prototype = classPrototypeData(lowerer, info, loc, { + kind: "dynCheck", + value, + type, + loc, + }); + if (prototype) + this.errorPrototypeHelper.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [prototype], + type: BOOL, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }); + changed = true; + } if ( this.serializesClasses && !this.jsonPrototypes.has(className) && @@ -1516,6 +1564,31 @@ export class ClassDynamicDispatch { } return { kind: "call", callee: cast.fn.name, args: [expr.value], type, loc: expr.loc }; } + if (expr.kind === "libCall" && expr.fn === "dyn.inheritsErrorPrototype") { + if (!this.errorPrototypeHelper) { + const loc = expr.loc; + this.errorPrototypeHelper = { + name: "%dyn.class.inheritsErrorPrototype", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: BOOL, + loc, + body: [ + { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, + ], + }; + lowerer.liftedFns.push(this.errorPrototypeHelper); + this.generated.add(this.errorPrototypeHelper); + changed = true; + } + return { + kind: "call", + callee: this.errorPrototypeHelper.name, + args: expr.args, + type: BOOL, + loc: expr.loc, + }; + } if ( expr.kind === "libCall" && expr.fn === "dyn.getPrototype" && @@ -5942,3 +6015,11 @@ const INHERITED_OBJECT_METHODS: ReadonlySet = new Set([ "hasOwnProperty", "propertyIsEnumerable", ]); + +/** Whether a class or one of its bases has a heritage evaluated at run + * time, whose prototype chain may reach Error.prototype. */ +function hasRuntimeHeritage(info: ClassInfo): boolean { + for (let current: ClassInfo | null = info; current; current = current.base) + if (current.callableBase) return true; + return false; +} diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index abbaf3a75..fd23b6db8 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -380,6 +380,13 @@ export function collectClassMethodMutations( markTarget(node.initializer, node); if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node)) lowerer.errorPrototypeValues = true; + // A heritage evaluated at run time may be Error, which links the + // class's instances to Error.prototype. + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + runtimeHeritage(lowerer, node) + ) + lowerer.errorPrototypeValues = true; if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { const base = unwrap(node.expression); const prototype = @@ -621,3 +628,22 @@ export function collectClassMethodMutations( if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field); } } + +/** Whether a class's `extends` expression is a value computed at run time: + * anything other than a name of a class declaration or of a builtin + * constructor (`extends Parent` over `const Parent = params.Parent ?? Object`, + * `extends make()`). */ +function runtimeHeritage( + lowerer: Lowerer, + node: ts.ClassDeclaration | ts.ClassExpression, +): boolean { + const heritage = node.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (heritage === undefined) return false; + if (!ts.isIdentifier(heritage) && !ts.isPropertyAccessExpression(heritage)) return true; + let symbol = lowerer.checker.getSymbolAtLocation(heritage); + if (symbol === undefined) return true; + if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol); + return !(symbol.flags & ts.SymbolFlags.Class) && !lowerer.isStdlibSymbol(symbol); +} diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index c8a8d6ba6..32cff333e 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -80,6 +80,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.inheritsErrorPrototype", "dyn.reflectGet", "dyn.reflectSet", ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index d1b9795d5..043ee7d48 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4504,15 +4504,13 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { return count; } -const globalThisWrites = new WeakMap>(); - /** The property names the program assigns on the global object, through * `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set { - const known = globalThisWrites.get(lowerer); + const known = lowerer.globalThisWrittenNames; if (known) return known; const names = new Set(); - globalThisWrites.set(lowerer, names); + lowerer.globalThisWrittenNames = names; for (const sf of lowerer.program.getImplementationSourceFiles()) { if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue; ts.walkPreorder(sf, (node) => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 83ac404d4..c3b555d09 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2969,9 +2969,14 @@ export class Lowerer { * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); /** Whether the program reads `Error.prototype`, the checked-dynamic - * object an ES5 constructor function can inherit from: only then can a - * value that is not an Error instance be `instanceof Error`. */ + * object an ES5 constructor function can inherit from, or has a class + * whose heritage is evaluated at run time (`extends (Base ?? Error)`): + * only then can a value that is not an Error instance be + * `instanceof Error`. */ errorPrototypeValues = false; + /** The property names the program assigns on the global object + * (property-access.ts), collected on first use. */ + globalThisWrittenNames: Set | null = null; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index df40573da..cf828c6f0 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -32,12 +32,13 @@ export function lowerModuleNamespaceElement( try { const body: IrStmt[] = []; const symbol = lowerer.checker.getSymbolAtLocation(source); - symbol?.getExports().forEach((exported, key) => { + for (const exported of symbol ? lowerer.checker.getExportsOfModule(symbol) : []) { + const key = ts.symbolName(exported); const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; - if (!(target.flags & ts.SymbolFlags.Value)) return; + if (!(target.flags & ts.SymbolFlags.Value)) continue; const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; if (!identifier || !ts.isIdentifier(identifier)) { @@ -50,7 +51,7 @@ export function lowerModuleNamespaceElement( kind: "strEq", negated: false, left: varRef("key", STRING, loc), - right: strLit(String(key), loc), + right: strLit(key, loc), type: { kind: "bool" }, loc, }, @@ -58,7 +59,7 @@ export function lowerModuleNamespaceElement( else_: null, loc, }); - }); + } body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); const fn: IrFunction = { name, @@ -111,10 +112,12 @@ export function moduleNamespaceObject( if (!symbol) return null; const exports: { key: string; identifier: ts.Identifier }[] = []; let exact = true; - symbol.getExports().forEach((exported, key) => { + // The module's resolved exports, which include `export *` members. + for (const exported of lowerer.checker.getExportsOfModule(symbol)) { + const key = ts.symbolName(exported); const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; - if (!(target.flags & ts.SymbolFlags.Value)) return; + if (!(target.flags & ts.SymbolFlags.Value)) continue; const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; const immutable = @@ -125,10 +128,10 @@ export function moduleNamespaceObject( (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0)); if (!identifier || !ts.isIdentifier(identifier) || !immutable) { exact = false; - return; + break; } - exports.push({ key: String(key), identifier }); - }); + exports.push({ key, identifier }); + } if (!exact) return null; exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); const name = `%module.object:${ns.type.moduleId}`; diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 9550d1dc2..eb2b9e20b 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -631,6 +631,14 @@ export class CheckerFacade { return aliased; } + /** A module symbol's exports, with the members of its `export *` + * declarations resolved (a symbol's own export table lists only the + * module's own declarations). */ + getExportsOfModule(symbol: Ts7Symbol): readonly Ts7Symbol[] { + this.ensureActive(); + return this.raw.getExportsOfModule(symbol); + } + getDeclaredTypeOfSymbol(symbol: Ts7Symbol): Type { this.ensureActive(); let type = this.cache.declaredTypeOfSymbol.get(symbol); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json index 0c768ef66..b8a66c735 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json @@ -2,6 +2,7 @@ "order": [ "/tests/corpus/7487-namespace-spread-untyped/shapes.js", "/tests/corpus/7487-namespace-spread-untyped/extra.js", + "/tests/corpus/7487-namespace-spread-untyped/all.js", "/tests/corpus/7487-namespace-spread-untyped/main.js" ], "diags": [] diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 57c9e4df4..b0abe20ef 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5146,10 +5146,17 @@ void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *argument scr_dyn_release(value); if (scr_exc_pending()) return; } - if (argc > 1 && argv[1]->kind == SCR_DYN_OBJ && scr_dyn_find_entry(argv[1], "cause", 5)) { - ScrDyn *cause = scr_dyn_obj_get(argv[1], "cause", 5); + /* InstallErrorCause: an options object with a `cause` property, own or + * inherited, defines the value its getter or data property reads. */ + ScrDyn *options = argc > 1 ? argv[1] : NULL; + if (options && options->kind != SCR_DYN_NULL && + (options->kind == SCR_DYN_FUNC || scr_dyn_is_object(options))) { ScrStr *key = scr_str_new("cause", 5); - scr_dyn_define_hidden(receiver, key, cause ? cause : scr_dyn_undefined()); + if (scr_dyn_in(options, key) && !scr_exc_pending()) { + ScrDyn *cause = scr_dyn_obj_read(options, "cause", 5); + if (cause && !scr_exc_pending()) scr_dyn_define_hidden(receiver, key, cause); + scr_dyn_release(cause); + } scr_str_release(key); if (scr_exc_pending()) return; } diff --git a/tests/corpus/7481-class-runtime-heritage-object-error.js b/tests/corpus/7481-class-runtime-heritage-object-error.js index 13d59dc63..32e17c640 100644 --- a/tests/corpus/7481-class-runtime-heritage-object-error.js +++ b/tests/corpus/7481-class-runtime-heritage-object-error.js @@ -22,3 +22,30 @@ try { } catch (err) { console.log(err instanceof E, err.message); } +console.log(e instanceof Error, new A() instanceof Error); +try { + throw new E("caught"); +} catch (err) { + console.log(err instanceof Error, err.message); +} +// The cause option is read like Error's: inherited and accessor causes +// count, and a throwing getter's exception propagates. +const inherited = new E("inherited", Object.create({ cause: "from prototype" })); +console.log(inherited.cause, Object.prototype.hasOwnProperty.call(inherited, "cause")); +let reads = 0; +const accessor = new E("accessor", { + get cause() { + reads++; + return "from getter"; + }, +}); +console.log(accessor.cause, reads, "cause" in new E("none", {}), "cause" in new E("undef", { cause: undefined })); +try { + new E("throws", { + get cause() { + throw new Error("getter failed"); + }, + }); +} catch (err) { + console.log(err.message); +} diff --git a/tests/corpus/7487-namespace-spread-untyped/all.js b/tests/corpus/7487-namespace-spread-untyped/all.js new file mode 100644 index 000000000..e317afdab --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/all.js @@ -0,0 +1,2 @@ +export * from "./shapes.js"; +export const own = "own"; diff --git a/tests/corpus/7487-namespace-spread-untyped/main.js b/tests/corpus/7487-namespace-spread-untyped/main.js index 021c00ac9..2352e844d 100644 --- a/tests/corpus/7487-namespace-spread-untyped/main.js +++ b/tests/corpus/7487-namespace-spread-untyped/main.js @@ -9,3 +9,8 @@ const z = untyped({ ...shapes, ...extra, extra }); console.log(Object.keys(z).join(",")); for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2)); console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size); +// A namespace with star re-exports holds the re-exported members too. +import * as all from "./all.js"; +const w = untyped({ ...all }); +console.log(Object.keys(w).join(","), w.circle(1).area.toFixed(2), w.own, new w.Box(2).size); +for (const key of ["circle", "own", "VERSION", "missing"]) console.log(key, typeof all[key]); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8b0b51b8..d6bee86d6 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1711,9 +1711,10 @@ describe("builtin constructor values (JS lane)", () => { test("members without a lowering stay refused instead of reading undefined", async () => { // Builtin constructors read as interned function values that model only // name, length and constructor. Every other static member or prototype - // read (direct, bracketed, `in`, through an alias or globalThis), and + // read (direct, bracketed, through an alias or globalThis), and // reflection on or redefinition of their members, must stay a located // SC2020/SC1090 refusal, never an undefined or otherwise wrong value. + // `key in Ctor` answers from the constructor's fixed keys. // Test262 lost 769 refusals to undefined reads before this was pinned. const reads = [ ["Map.prototype", "Map.prototype"], @@ -1731,7 +1732,6 @@ describe("builtin constructor values (JS lane)", () => { ], ["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"], ["Map['prototype']", 'Map["prototype"]'], - ["'prototype' in Map", '"prototype" in Map'], ["globalThis.Map.prototype", "globalThis.Map.prototype"], ["Map alias prototype", "MapAlias.prototype"], ["Date alias prototype", "DateAlias.prototype"], @@ -1754,6 +1754,7 @@ console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.lengt console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function); const promisePrototype = Promise.prototype; console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise); +console.log("prototype" in Map, "groupBy" in Map, "captureStackTrace" in Error, "nope" in Map); `; const r = await compileAndRun("builtin-constructor-members", source, "cjs"); expect(r.stderr).toBe(""); @@ -1768,11 +1769,13 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot ), ), ); - // Promise.prototype reads as an object holding the runtime's methods. + // Promise.prototype reads as an object holding the runtime's methods, + // and `in` on a builtin constructor answers from its fixed keys. expect(lines.slice(reads.length)).toEqual([ "7 2 1 1 0 0", "Date Map true true", "object function true", + "true true true false", ]); }); }); From 84eba882764b4f39a44024adc1568b976e8808d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:13:46 -0500 Subject: [PATCH 108/169] Convert typed arrays to BigInt through their element text BigInt(typedArray) compiled in JavaScript but stopped at run time with SC2020. Node converts the typed array through ToPrimitive to its elements joined with "," and parses that text: one element reads as its value, an empty array as 0n, and several elements, fractions, NaN, Infinity or an exponent form throw the SyntaxError for the joined text. - BigInt of a typed array lowers to bigint.parse of its "," join, the same conversion Node performs. - A union whose arms are typed arrays and other BigInt sources dispatches each typed-array arm the same way. Corpus 7403 covers every supported element kind, negative and clamped values, float formatting (2 ** 60 reads through its shortest decimal text), empty arrays and views, the SyntaxError cases and results in arithmetic, and matches Node. Refs #1647 Co-authored-by: Quino Terrasa <26749475+espetro@users.noreply.github.com> --- .../src/frontend/lowering/lower-calls.ts | 15 ++++++ .../7403-bigint-of-typed-arrays.mjs.json | 6 +++ tests/corpus/7403-bigint-of-typed-arrays.mjs | 46 +++++++++++++++++++ .../7403-bigint-of-typed-arrays.mjs.json | 1 + 4 files changed, 68 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json create mode 100644 tests/corpus/7403-bigint-of-typed-arrays.mjs create mode 100644 tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index d18761f98..8424f28c4 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2400,6 +2400,20 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc }; + // A typed array converts through ToPrimitive to its elements joined with + // ",", which BigInt then parses: one element reads as that element, none + // as 0n, and several throw the SyntaxError for the joined text. + if (arg.type.kind === "bytes") { + const text: IrExpr = { + kind: "bytesIntrinsic", + method: "join", + receiver: arg, + args: [{ kind: "strLit", value: ",", type: STRING, loc }], + type: STRING, + loc, + }; + return { kind: "libCall", fn: "bigint.parse", args: [text], type: BIGINT_T, loc }; + } if (arg.type.kind === "bool") { return { kind: "ternary", @@ -2440,6 +2454,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || + arm.kind === "bytes" || isUnitType(arm), ) ) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json new file mode 100644 index 000000000..476611adb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7403-bigint-of-typed-arrays.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7403-bigint-of-typed-arrays.mjs b/tests/corpus/7403-bigint-of-typed-arrays.mjs new file mode 100644 index 000000000..77ef0cd2e --- /dev/null +++ b/tests/corpus/7403-bigint-of-typed-arrays.mjs @@ -0,0 +1,46 @@ +// BigInt of a typed array converts the array to its elements joined with +// "," and parses that text, as in Node: one element reads as its value, +// an empty array as 0n, and anything else throws the SyntaxError for the +// joined text. Every element kind, negative and clamped values, float +// formatting, views and the results in arithmetic. + +const one = [ + new Int8Array([-5]), + new Uint8Array([7]), + new Uint8ClampedArray([300]), + new Int16Array([-30000]), + new Uint16Array([65535]), + new Int32Array([-2147483648]), + new Uint32Array([4294967295]), + new Float32Array([-0]), + new Float64Array([2 ** 60]), +]; +for (const view of one) console.log(view.constructor.name, BigInt(view)); + +const empty = [new Uint8Array(0), new Float64Array([]), new Int32Array(8).subarray(3, 3)]; +for (const view of empty) console.log("empty", view.constructor.name, BigInt(view)); + +const bad = [ + new Uint8Array([1, 2]), + new Float64Array([1.5]), + new Float32Array([NaN]), + new Float64Array([1e21]), + new Int16Array([3, -4, 5]), + new Float64Array([Infinity]), +]; +for (const view of bad) { + try { + console.log("unexpected", BigInt(view)); + } catch (e) { + console.log(view.constructor.name, e instanceof SyntaxError, e.message); + } +} + +const bytes = new Uint8Array([9, 8, 7]); +const window = bytes.subarray(1, 2); +let total = 0n; +for (let i = 0; i < 100; i++) { + bytes[1] = i; + total += BigInt(window) * 3n; +} +console.log("window", BigInt(window), total, BigInt(bytes.subarray(2)) ** 2n); diff --git a/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json b/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json new file mode 100644 index 000000000..754441b3e --- /dev/null +++ b/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json @@ -0,0 +1 @@ +{ "plain": 1738, "san": 3053 } From 9ab876faa9771df730c446628aad8665814bf68e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:15:30 -0700 Subject: [PATCH 109/169] Compile the @hono/node-server shim under any program's stream types The shim compiles inside each program that imports the package, and under some lib and @types/node settings ReadableStreamReadResult lowered as unknown while reader.read() produced a record, so the buffered-read path refused (SC2003). The shim spells out its read-result record and types the body reader. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/hono-node-server/v2.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index 0402fe0af..94a9c8d85 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -185,7 +185,10 @@ function buildOutgoingHttpHeaders( return res; } -type ReadResult = ReadableStreamReadResult; +/* Spelled out rather than ReadableStreamReadResult, whose + * declaration depends on the program's lib and @types/node settings: the + * shim compiles inside every program that imports the package. */ +type ReadResult = { done: boolean; value?: Uint8Array }; function readWithoutBlocking(read: Promise): Promise { return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]); @@ -220,7 +223,7 @@ async function pump( outgoing.end(); return; } - outgoing.write(value); + outgoing.write(value!); read = reader.read(); } } finally { @@ -352,7 +355,7 @@ async function writeResponse( const body = res.body; const record = buildOutgoingHttpHeaders(res.headers, body !== null); if (body !== null) { - const reader = body.getReader(); + const reader: ReadableStreamDefaultReader = body.getReader(); const values: Uint8Array[] = []; let done = false; let pending: Promise | undefined = undefined; From 143f2e11e9c11dada9f75a75fff4b80978196bd7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:15:57 -0500 Subject: [PATCH 110/169] Keep an own stackTraceLimit on the Error value ahead of the limit --- .../corpus/7603-error-stack-limit-own.js.json | 6 +++++ .../7604-error-stack-limit-frozen.js.json | 6 +++++ packages/runtime/src/scr_json.c | 23 +++++++++++++++++-- tests/corpus/7603-error-stack-limit-own.js | 13 +++++++++++ tests/corpus/7604-error-stack-limit-frozen.js | 9 ++++++++ .../7603-error-stack-limit-own.js.json | 1 + .../7604-error-stack-limit-frozen.js.json | 1 + 7 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json create mode 100644 tests/corpus/7603-error-stack-limit-own.js create mode 100644 tests/corpus/7604-error-stack-limit-frozen.js create mode 100644 tests/harness/corpus-costs/7603-error-stack-limit-own.js.json create mode 100644 tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json new file mode 100644 index 000000000..5ed656fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7603-error-stack-limit-own.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json new file mode 100644 index 000000000..a437ff57e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7604-error-stack-limit-frozen.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 074fe3eae..bc6c9338a 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1878,6 +1878,17 @@ static bool scr_dyn_is_stack_limit(const ScrDyn *d, const char *key, size_t key_ scr_error_ctor_values[0] && d->v.fn.clo == scr_error_ctor_values[0]->v.fn.clo; } +/* An own `stackTraceLimit` on the Error value, assigned a non-number or + * defined with Object.defineProperty, replaces the limit for reads and + * writes through the value; writes to it keep its descriptor. */ +static bool scr_dyn_stack_limit_shadowed(const ScrDyn *d) { + ScrClosure *owner = scr_closure_identity(d->v.fn.clo); + ScrDyn *table = owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL; + bool own = table && scr_dyn_find_entry(table, "stackTraceLimit", 15); + scr_dyn_release(table); + return own; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -9000,12 +9011,19 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; - if (value->kind == SCR_DYN_NUM && scr_dyn_is_stack_limit(recv, key->data, key->len)) { + bool limit = scr_dyn_is_stack_limit(recv, key->data, key->len); + /* A frozen Error keeps the limit read-only. */ + if (limit && value->kind == SCR_DYN_NUM && !scr_dyn_stack_limit_shadowed(recv) && !scr_dyn_is_frozen(recv)) { scr_stack_limit_set(value->v.num); return; } ScrDyn *table = scr_dyn_fn_properties(recv); scr_dyn_object_key_set(table, key, value, recv); + /* A number the own data property takes also sets the limit. */ + ScrDynEntry *own = limit && value->kind == SCR_DYN_NUM && !scr_exc_pending() + ? scr_dyn_find_entry(table, key->data, key->len) + : NULL; + if (own && own->value == value) scr_stack_limit_set(value->v.num); scr_dyn_release(table); return; } @@ -11785,7 +11803,8 @@ static ScrDyn *scr_function_constructor(uint32_t kind) { /* Keyed read on a FUNC node (see scr_runtime.h): own props first, then * the function-instance built-ins name/length. +1 or NULL. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { - if (scr_dyn_is_stack_limit(d, key, key_len)) return scr_dyn_new_num(scr_stack_limit_get()); + if (scr_dyn_is_stack_limit(d, key, key_len) && !scr_dyn_stack_limit_shadowed(d)) + return scr_dyn_new_num(scr_stack_limit_get()); ScrClosure *owner = scr_closure_identity(d->v.fn.clo); if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props) owner->props = scr_box_retain(d->v.fn.class_obj->static_data); diff --git a/tests/corpus/7603-error-stack-limit-own.js b/tests/corpus/7603-error-stack-limit-own.js new file mode 100644 index 000000000..dfd4b7cdf --- /dev/null +++ b/tests/corpus/7603-error-stack-limit-own.js @@ -0,0 +1,13 @@ +// An own stackTraceLimit on the Error constructor held as a value (a +// non-number assigned, or one defined with Object.defineProperty) answers +// reads through the value, and writes to it keep its descriptor. +const E = /** @type {any} */ (Error); +E.stackTraceLimit = "deep"; +console.log(E.stackTraceLimit, typeof E.stackTraceLimit); +E.stackTraceLimit = 2; +console.log(E.stackTraceLimit, Error.stackTraceLimit); +Object.defineProperty(E, "stackTraceLimit", { value: 6, writable: false, configurable: true }); +try { + E.stackTraceLimit = 1; +} catch {} +console.log(E.stackTraceLimit); diff --git a/tests/corpus/7604-error-stack-limit-frozen.js b/tests/corpus/7604-error-stack-limit-frozen.js new file mode 100644 index 000000000..9c73d1403 --- /dev/null +++ b/tests/corpus/7604-error-stack-limit-frozen.js @@ -0,0 +1,9 @@ +// A frozen Error keeps stackTraceLimit read-only through the constructor +// held as a value. +const E = /** @type {any} */ (Error); +E.stackTraceLimit = 3; +Object.freeze(E); +try { + E.stackTraceLimit = 1; +} catch {} +console.log(E.stackTraceLimit, Error.stackTraceLimit); diff --git a/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } diff --git a/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } From 9a452b8129ffe08d16939db5b3614c8279e0553c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:24:18 -0700 Subject: [PATCH 111/169] Keep replaceable method results that have no dynamic form native A method whose name an untyped write can replace is called through the instance's property lookup: a ternary between the dynamic call of an installed replacement and the native call, both boxed into a dynamic value and converted back to the method's result type. A result with no dynamic form, such as an Iterable (its generator arm does not box), stayed unboxed in the dynamic ternary, which failed IR validation with "ternary else-branch: expected dyn, got union". Methods of generic class instantiations always take this path once any write can replace their name, because their prototype data is not modeled, so importing zod (whose JavaScript has unknown-key writes) broke every such call. Such a result now keeps its native type: the native call's value is used as it is, and only a replacement's dynamic result takes the checked conversion into the result type. Results that box are lowered as before. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-callbacks.ts | 72 ++++++++++++------ ...terable-replaceable-method-results.ts.json | 6 ++ ...565-iterable-replaceable-method-results.ts | 76 +++++++++++++++++++ ...terable-replaceable-method-results.ts.json | 1 + 4 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json create mode 100644 tests/corpus/7565-iterable-replaceable-method-results.ts create mode 100644 tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 6ca685684..b51158022 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -6,12 +6,14 @@ import { STRING, VOID, isClassOwnEnumerableFieldName, + typeEquals, type IrExpr, type IrStmt, type SrcLoc, } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; -import type { Lowerer } from "./lowerer.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; import { findMethodOn, type ClassInfo } from "./lower-classes.js"; import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; import { observedPrototypeMethod } from "./class-prototypes.js"; @@ -186,27 +188,53 @@ export function classCallbackCall( loc, }; const observed = observedSlot(lowerer, receiver, name, loc); - const result: IrExpr = observed - ? call - : { - kind: "ternary", - cond: varRef(present.id, BOOL, loc), - then: call, - else_: - fallback.type.kind === "void" - ? { - kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: fallback, loc }], - result: dynUndefinedExpr(loc), - type: DYN, - loc, - } - : lowerer.coerceToExpected(fallback, DYN), - type: DYN, - loc, - }; - const answer = - fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + const boxed = + observed || fallback.type.kind === "void" ? null : lowerer.coerceToExpected(fallback, DYN); + let answer: IrExpr; + if (boxed !== null && boxed.type.kind !== "dyn") { + // A result with no checked-dynamic form (an Iterable, whose + // 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); + if (!typeEquals(converted.type, fallback.type)) { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + loc, + `calling replaceable method '${calleeName}' with a '${lowerer.fmt(fallback.type)}' result`, + ), + ); + throw new PoisonError(); + } + answer = { + kind: "ternary", + cond: varRef(present.id, BOOL, loc), + then: converted, + else_: fallback, + type: fallback.type, + loc, + }; + } else { + const result: IrExpr = observed + ? call + : { + kind: "ternary", + cond: varRef(present.id, BOOL, loc), + then: call, + else_: boxed ?? { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: fallback, loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + type: DYN, + loc, + }; + answer = + fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + } return { kind: "seqExpr", stmts: [ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json new file mode 100644 index 000000000..60631a82f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7565-iterable-replaceable-method-results.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7565-iterable-replaceable-method-results.ts b/tests/corpus/7565-iterable-replaceable-method-results.ts new file mode 100644 index 000000000..4e71a0dcc --- /dev/null +++ b/tests/corpus/7565-iterable-replaceable-method-results.ts @@ -0,0 +1,76 @@ +// A method returning Iterable whose name an untyped write can replace +// (`holder.candidates = ...` through `any`) is called through the +// instance's property lookup: the native method runs while the slot is +// unchanged, and a replacement installed on an instance runs instead, +// its result iterated as an Iterable. + +interface Lookup { + readonly index: string; + readonly key: string; +} +interface Query { + readonly lookups?: readonly Lookup[]; +} +interface Row { + id: string; + n: number; +} + +class Repo { + private readonly rows = new Map(); + add(row: T): void { + this.rows.set(row.id, row); + } + candidates(lookups: readonly Lookup[]): Iterable { + if (lookups.length === 0) return this.rows.values(); + const out: T[] = []; + for (const row of this.rows.values()) if (lookups.every((l) => l.key === row.id)) out.push(row); + return out; + } + query(query: Query): T[] { + const matches: T[] = []; + for (const row of this.candidates(query.lookups ?? [])) matches.push(row); + return matches; + } + ids(query: Query): string { + let out = ""; + for (const row of this.candidates(query.lookups ?? [])) out += (out ? "," : "") + row.id; + return out; + } +} + +function seed(): Repo { + const r = new Repo(); + r.add({ id: "a", n: 1 }); + r.add({ id: "b", n: 2 }); + r.add({ id: "c", n: 3 }); + return r; +} + +const r = seed(); +console.log(r.query({}).length, r.query({ lookups: [{ index: "i", key: "b" }] })); +console.log(r.ids({}), r.ids({ lookups: [{ index: "i", key: "c" }] })); + +// An untyped write of the same name: on an unrelated object first, then +// on one Repo instance, whose own property shadows the class method. +const holder: any = {}; +holder.candidates = (): Row[] => []; +console.log(typeof holder.candidates); + +const replaced = seed(); +const untyped: any = replaced; +untyped.candidates = function (lookups: readonly Lookup[]): Row[] { + return [{ id: "z" + lookups.length, n: 9 }]; +}; +console.log(replaced.query({}), replaced.query({ lookups: [{ index: "i", key: "a" }] })); +console.log(replaced.ids({})); + +// A generator replacement is single-pass, as in JavaScript. +untyped.candidates = function* (): Generator { + yield { id: "g1", n: 10 }; + yield { id: "g2", n: 11 }; +}; +console.log(replaced.query({}).map((row) => row.id + ":" + row.n)); + +// The untouched instance still runs the class method. +console.log(r.ids({})); diff --git a/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json b/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json new file mode 100644 index 000000000..6d81df217 --- /dev/null +++ b/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json @@ -0,0 +1 @@ +{ "plain": 8000, "san": 14000 } From f030e5ad44418e398dbfc395a054d3ae04bd3a28 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:36:11 -0500 Subject: [PATCH 112/169] Check with tsc-ts 0.0.8 on every platform tsc-ts 0.0.8's API server serves on the named pipe the Node host connects over on Windows (tsc-ts --api --pipe), and the native compiler already starts it over its standard streams there, so tsc-ts is the default checker on every platform. SCRIPTC_CHECKER=tsgo still selects tsgo. The compiler depends on tsc-ts 0.0.8, which the release-age exclusions admit with its platform packages and @scriptc/threads 0.4.2. The Windows CLI smoke test runs with both checkers, and verify-native-cli.mjs --run checks that a distribution's tsc-ts runs on its platform (tsc-ts.exe on Windows). --- .github/workflows/ci.yml | 19 +++-- docs/content/docs/cli.mdx | 2 +- packages/compiler/package.json | 2 +- .../src/frontend/ts7/checker-backend.test.ts | 14 ++-- .../src/frontend/ts7/checker-backend.ts | 12 +-- .../compiler/src/native/toolchain.test.ts | 6 +- packages/compiler/src/native/toolchain.ts | 8 +- pnpm-lock.yaml | 74 +++++++++---------- pnpm-workspace.yaml | 18 ++--- scripts/verify-native-cli.mjs | 10 +++ 10 files changed, 86 insertions(+), 79 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a100e374..f131747fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -773,17 +773,22 @@ jobs: run: >- pnpm exec vitest run tests/harness/worktree-files.test.ts --testNamePattern "workspace reset keeps Linux Sandbox paths POSIX" + # The Node host checks with tsc-ts, the default, over the named pipe it + # serves on Windows, and with tsgo. - name: Windows scriptc run smoke shell: pwsh run: | zig version - $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $actual = ($output -join "`n").Trim() - if ($actual -ne "hello world") { - throw "unexpected scriptc output: $actual" + foreach ($checker in @("tsc-ts", "tsgo")) { + $env:SCRIPTC_CHECKER = $checker + $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $actual = ($output -join "`n").Trim() + if ($actual -ne "hello world") { + throw "unexpected scriptc output with ${checker}: $actual" + } } - name: Windows PE subsystem selection env: diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 8c57d2e2a..8b0353620 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -206,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
A runtime pack directory to use instead of an installed @scriptc/runtime-<target> package.
SCRIPTC_CHECKER
-
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is tsgo, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.
+
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
```console diff --git a/packages/compiler/package.json b/packages/compiler/package.json index a1d66963d..a358ae545 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.7", + "tsc-ts": "0.0.8", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/frontend/ts7/checker-backend.test.ts b/packages/compiler/src/frontend/ts7/checker-backend.test.ts index e37357afa..cd37bafff 100644 --- a/packages/compiler/src/frontend/ts7/checker-backend.test.ts +++ b/packages/compiler/src/frontend/ts7/checker-backend.test.ts @@ -24,15 +24,11 @@ function writePackage(directory: string, name: string, executable?: string): voi writeFileSync(join(directory, "lib", executable), ""); } -test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => { - expect(checkerBackend({}, "linux")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts"); - expect(checkerBackend({}, "darwin")).toBe("tsc-ts"); - expect(checkerBackend({}, "win32")).toBe("tsgo"); - expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo"); +test("tsc-ts is the default checker, and SCRIPTC_CHECKER selects one", () => { + expect(checkerBackend({})).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "" })).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" })).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" })).toBe("tsgo"); expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow( 'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"', ); diff --git a/packages/compiler/src/frontend/ts7/checker-backend.ts b/packages/compiler/src/frontend/ts7/checker-backend.ts index 14418cc94..0b32f24f6 100644 --- a/packages/compiler/src/frontend/ts7/checker-backend.ts +++ b/packages/compiler/src/frontend/ts7/checker-backend.ts @@ -4,17 +4,13 @@ import { fileURLToPath } from "node:url"; /** The TypeScript 7 API servers a compile can check with: tsc-ts, the default, * and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`. - * Both serve the same API and give the same results. On Windows the default is - * tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's - * API server has no named-pipe transport yet. */ + * Both serve the same API and give the same results. On Windows the Node host + * connects to either over a named pipe the server creates (rpc-process.ts). */ export type CheckerBackend = "tsgo" | "tsc-ts"; -export function checkerBackend( - env: NodeJS.ProcessEnv = process.env, - platform: string = process.platform, -): CheckerBackend { +export function checkerBackend(env: NodeJS.ProcessEnv = process.env): CheckerBackend { const value = env["SCRIPTC_CHECKER"]; - if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts"; + if (value === undefined || value === "") return "tsc-ts"; if (value === "tsc-ts") return "tsc-ts"; if (value === "tsgo") return "tsgo"; throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`); diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index 0ee434300..ad4570c0c 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -99,11 +99,11 @@ test("native toolchain assets resolve from the physical manifest through package const direct = loadNativeToolchain(path); const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json")); expect(viaLink).toEqual(direct); - expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe")); + expect(viaLink.ts7Executable).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); expect( - loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" }) + loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsgo" }) .ts7Executable, - ).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); + ).toBe(join(assets, "typescript/lib/tsc.exe")); expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe")); expect( loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index fbcd94251..5abbb329e 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -177,11 +177,11 @@ export function loadNativeToolchain( : [...target.defaultLinkerArgs]; return { compilerVersion: manifest.compiler_version, - // tsc-ts, the default checker except on Windows, comes with the distribution; - // a development build takes it from the tsc-ts package installed with the - // compiler or for the working directory. SCRIPTC_CHECKER selects a checker. + // tsc-ts, the default checker, comes with the distribution; a development + // build takes it from the tsc-ts package installed with the compiler or for + // the working directory. SCRIPTC_CHECKER selects a checker. ts7Executable: - checkerBackend(env, host.platform) === "tsgo" + checkerBackend(env) === "tsgo" ? pathFrom(root, manifest.ts7) : manifest.tsc_ts !== undefined ? pathFrom(root, manifest.tsc_ts) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 589de2a51..525994ba1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,8 +110,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.8 + version: 0.0.8 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1360,8 +1360,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.4.1': - resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==} + '@scriptc/threads@0.4.2': + resolution: {integrity: sha512-nR9FUxpOpgtTmJu6dejttElBL+JJTAZzJ2DZM+eNoujmLx9VpdlAVdgj3cu/MDsTUg/1sIUBq1ME1csC/Aa6yg==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1377,42 +1377,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.7': - resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==} + '@tsc-ts/darwin-arm64@0.0.8': + resolution: {integrity: sha512-+hB9yWk58lwZHGrVEzqEC4D7MQW7Vd2z7blV3TULIKZPTMwdf8aT7XFAmfP48PoGff0H9yuQKyRXJl9I+sQv9w==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.7': - resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==} + '@tsc-ts/darwin-x64@0.0.8': + resolution: {integrity: sha512-yz4UddicqMXe+T7nV7ftpf155lkiK6gIPNwSB8rH9dlwiV6a17CO/vpSk627QLMqnfGNicB6YwLMk/SJ9moPdw==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.7': - resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==} + '@tsc-ts/linux-arm64-gnu@0.0.8': + resolution: {integrity: sha512-LHqfT9mQ4tusPdWmnanCvC8/YBOF5bHM5h5863a3t2HTgFVdps1Qwb0yzrW5DDbojIhQ0Z+GupxtXVUQcV0Q/Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.7': - resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==} + '@tsc-ts/linux-arm64-musl@0.0.8': + resolution: {integrity: sha512-I0CC3xZMwoQM6OiVsubRHb0VI/RrJpneb33kDQhadYHq3taPos71KwP+CbCXNu88rNsDsyn7D1oCVfJb73g4Mw==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.7': - resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==} + '@tsc-ts/linux-x64-gnu@0.0.8': + resolution: {integrity: sha512-PLERuwUuxT92x0BZAMQubiQP5jhlqoLmswaZWqcvfKAZyBp0TbWiVOmuNnDbJozvBaNUrfdGPb9suiQ+Me7M6A==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.7': - resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==} + '@tsc-ts/linux-x64-musl@0.0.8': + resolution: {integrity: sha512-zDl36l1uyWc0mQniOAMZmkVD9TrlFExHlDT0fL8JGD4XLgx+bRe+To38yP0KSBehXBzjDNn8xrEoJ+3RP6mYwQ==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.7': - resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==} + '@tsc-ts/win32-x64-msvc@0.0.8': + resolution: {integrity: sha512-9gO+pFoLiIxITlAE2P6JL8ihZwns7z/2Raw5+38RqLzHmnSsRN47+H4p/4EqQsLW5/TTsAo9uSeSkf3EuRg+XA==} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.7: - resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==} + tsc-ts@0.0.8: + resolution: {integrity: sha512-EiyDziYOryBg/YEvev/mKCn+hD4M6VFjH7VP9nv2WadDKYaPrdb+a3hP+wYr/UsDWNE7AdL7YGnjZppeXlXy6g==} engines: {node: '>=20'} hasBin: true @@ -3439,7 +3439,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.4.1': {} + '@scriptc/threads@0.4.2': {} '@sinclair/typebox@0.25.24': {} @@ -3454,25 +3454,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.7': + '@tsc-ts/darwin-arm64@0.0.8': optional: true - '@tsc-ts/darwin-x64@0.0.7': + '@tsc-ts/darwin-x64@0.0.8': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.7': + '@tsc-ts/linux-arm64-gnu@0.0.8': optional: true - '@tsc-ts/linux-arm64-musl@0.0.7': + '@tsc-ts/linux-arm64-musl@0.0.8': optional: true - '@tsc-ts/linux-x64-gnu@0.0.7': + '@tsc-ts/linux-x64-gnu@0.0.8': optional: true - '@tsc-ts/linux-x64-musl@0.0.7': + '@tsc-ts/linux-x64-musl@0.0.8': optional: true - '@tsc-ts/win32-x64-msvc@0.0.7': + '@tsc-ts/win32-x64-msvc@0.0.8': optional: true '@tybys/wasm-util@0.10.3': @@ -4813,17 +4813,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.7: + tsc-ts@0.0.8: dependencies: - '@scriptc/threads': 0.4.1 + '@scriptc/threads': 0.4.2 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.7 - '@tsc-ts/darwin-x64': 0.0.7 - '@tsc-ts/linux-arm64-gnu': 0.0.7 - '@tsc-ts/linux-arm64-musl': 0.0.7 - '@tsc-ts/linux-x64-gnu': 0.0.7 - '@tsc-ts/linux-x64-musl': 0.0.7 - '@tsc-ts/win32-x64-msvc': 0.0.7 + '@tsc-ts/darwin-arm64': 0.0.8 + '@tsc-ts/darwin-x64': 0.0.8 + '@tsc-ts/linux-arm64-gnu': 0.0.8 + '@tsc-ts/linux-arm64-musl': 0.0.8 + '@tsc-ts/linux-x64-gnu': 0.0.8 + '@tsc-ts/linux-x64-musl': 0.0.8 + '@tsc-ts/win32-x64-msvc': 0.0.8 tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ceb1506e9..e74d40442 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,15 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.7" - - "@tsc-ts/darwin-x64@0.0.7" - - "@tsc-ts/linux-arm64-gnu@0.0.7" - - "@tsc-ts/linux-arm64-musl@0.0.7" - - "@tsc-ts/linux-x64-gnu@0.0.7" - - "@tsc-ts/linux-x64-musl@0.0.7" - - "@tsc-ts/win32-x64-msvc@0.0.7" - - "tsc-ts@0.0.7" - - "@scriptc/threads@0.4.1" + - "@tsc-ts/darwin-arm64@0.0.8" + - "@tsc-ts/darwin-x64@0.0.8" + - "@tsc-ts/linux-arm64-gnu@0.0.8" + - "@tsc-ts/linux-arm64-musl@0.0.8" + - "@tsc-ts/linux-x64-gnu@0.0.8" + - "@tsc-ts/linux-x64-musl@0.0.8" + - "@tsc-ts/win32-x64-msvc@0.0.8" + - "tsc-ts@0.0.8" + - "@scriptc/threads@0.4.2" allowBuilds: esbuild: true overrides: diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index 115573b5c..92de57938 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -94,5 +94,15 @@ if (process.argv.includes("--run")) { }).trim(); if (version !== identity.version) throw new Error(`native command reported ${version}, expected ${identity.version}`); + // tsc-ts, the default checker, runs on the distribution's own platform. + const tscTs = resolve(dirname(binary), hostPath(toolchain.tsc_ts)); + if (windows && !tscTs.endsWith("tsc-ts.exe")) + throw new Error(`tsc_ts is not tsc-ts.exe: ${tscTs}`); + const tscTsVersion = execFileSync(tscTs, ["--version"], { + encoding: "utf8", + env: { ...process.env, PATH: "" }, + }).trim(); + if (!tscTsVersion.startsWith("Version ")) + throw new Error(`the distribution's tsc-ts reported ${JSON.stringify(tscTsVersion)}`); } console.log(`${identity.name}@${identity.version}: native distribution verified`); From 1e35b1c56c46d743ed7c4fb010b844d0222572b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:36:34 -0500 Subject: [PATCH 113/169] Build the native compiler again The self-hosted compiler spreads only known record shapes and tests a value against null with == or != only as a plain read (SC1090). rewriteCandidates in int32-arrays.ts spread whole IR statement and expression unions: each spread now names one kind. The loop and block cases spread per kind, the hoisted read and sequence rewrites narrow with an if, switch cases spread through a typed helper, and the inlined ternary, a fresh copy, takes the call's location directly. The checked int32 array test in int32-slots.ts binds the range it reads first. --- packages/compiler/src/ir/int32-arrays.ts | 21 +++++++++++++++++---- packages/compiler/src/ir/int32-slots.ts | 5 ++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index bdb3f21e2..02de7268f 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -1,4 +1,6 @@ import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; + +type SwitchCase = Extract["cases"][number]; import { reparseExpr } from "./serialize.js"; import { everyExprChild, @@ -382,20 +384,26 @@ function rewriteCandidates( if (!call || simple(call.args[1]!)) return [s]; const { decl, ref } = bind(call.args[1]!); const read: IrExpr = { ...call, args: [call.args[0]!, ref] }; - return [decl, s.kind === "varDecl" ? { ...s, init: read } : { ...s, value: read }]; + if (s.kind === "varDecl") return [decl, { ...s, init: read }]; + return [decl, { ...s, value: read }]; }); + const hoistCase = (c: SwitchCase): SwitchCase => ({ ...c, body: hoist(c.body) }); const lists = (s: IrStmt): IrStmt => { switch (s.kind) { case "if": return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) }; case "block": + return { ...s, body: hoist(s.body) }; case "while": + return { ...s, body: hoist(s.body) }; case "doWhile": + return { ...s, body: hoist(s.body) }; case "for": + return { ...s, body: hoist(s.body) }; case "forOf": return { ...s, body: hoist(s.body) }; case "switch": - return { ...s, cases: s.cases.map((c) => ({ ...c, body: hoist(c.body) })) }; + return { ...s, cases: s.cases.map((c) => hoistCase(c)) }; case "tryCatch": return { ...s, @@ -425,7 +433,10 @@ function rewriteCandidates( }; } const call = readerCall(e); - if (!call) return e.kind === "seqExpr" ? { ...e, stmts: hoist(e.stmts) } : e; + if (!call) { + if (e.kind === "seqExpr") return { ...e, stmts: hoist(e.stmts) }; + return e; + } const helper = readers.get(call.callee)!; const array = call.args[0]!; const [arrayParam, indexParam] = helper.params; @@ -448,7 +459,9 @@ function rewriteCandidates( }, stmt: (x) => x, }); - const typed: IrExpr = { ...body, loc: e.loc }; + // The ternary is a fresh copy (reparseExpr): it takes the call's location. + const typed = body; + typed.loc = e.loc; return stmts.length === 0 ? typed : { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc }; diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index e6491ba80..4a2abc5b0 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -1131,7 +1131,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): const key = `l:${fn.name}\0${id}`; if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; slots.dropInt32Elements(key); - const checkable = (value: IrExpr): boolean => int32(value) || ranges.get(value) != null; + const checkable = (value: IrExpr): boolean => { + const range = ranges.get(value); + return int32(value) || range != null; + }; if (values.every(checkable) && checkableInt32Allocations(fn, id)) slots.checkedInt32Arrays.add(`${fn.name}\0${id}`); else reason(`array:${fn.name}\0${id}`, "an element store is not int32"); From 0b969479c3a66c0974892476ad69127ba74817fe Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:21:31 -0700 Subject: [PATCH 114/169] Build fresh literals a JavaScript callee may reshape as plain objects A fresh TypeScript data literal passed to a checked-dynamic slot became a typed view whose record layout requires every property, so a JavaScript callee deleting one (zod 4's `normalizeParams` deletes `params.message` from `.refine(fn, { message })`) threw "Cannot delete property". When the callee is implemented in JavaScript (a call through an untyped value, an untyped parameter of a JavaScript function, or a non-library `.d.ts` signature whose parameter is untyped or makes one of the literal's properties optional), the literal has no typed alias and is now built as the plain object the callee receives. Standard-library and TypeScript callees, and literals with methods or accessors, keep their native storage. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 48 ++++++++++++++++++- .../main.ts.json | 7 +++ .../7603-fresh-literal-untyped-param/lib.js | 12 +++++ .../7603-fresh-literal-untyped-param/main.ts | 18 +++++++ .../node_modules/params-kit/lib/index.d.ts | 10 ++++ .../node_modules/params-kit/lib/index.js | 11 +++++ .../node_modules/params-kit/package.json | 1 + .../main.ts.json | 1 + 8 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/lib.js create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/main.ts create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json create mode 100644 tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 93c6246d9..47db82f05 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2405,6 +2405,51 @@ export class Lowerer { return fallback ?? null; } + /** A fresh TypeScript data literal (`{ message: "m" }`) passed to a + * checked-dynamic parameter of a callee implemented in JavaScript, whose + * contract lets it reshape the object: a call through an untyped value, an + * untyped parameter of a JavaScript function, or a package-declared + * parameter type (a non-library `.d.ts`) that makes one of the literal's + * properties optional (zod's `normalizeParams` deletes `params.message`). + * No typed alias of a fresh literal exists, so it is built as the plain + * object the callee receives instead of a view whose record layout refuses + * that delete. Standard-library and TypeScript callees, and literals with + * methods or accessors, keep their native storage. */ + private reshapeableContextualLiteral( + literal: ts.ObjectLiteralExpression, + contextual: readonly ts.Type[], + ): boolean { + const call = literal.parent; + if ( + call === undefined || + !(ts.isCallExpression(call) || ts.isNewExpression(call)) || + !call.arguments?.nodes.includes(literal) || + literal.properties.nodes.length === 0 || + !literal.properties.nodes.every( + (prop) => + (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && + (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)), + ) + ) + return false; + const declaration = this.checker.getResolvedSignature(call)?.declaration; + const file = declaration ? ts.getSourceFileOfNode(declaration) : undefined; + const untyped = contextual.some((part) => part.flags & ts.TypeFlags.Any); + if (file === undefined) return untyped; + if (isJsSourceFile(file)) return untyped; + if (!file.isDeclarationFile || this.isStdlibFile(file)) return false; + if (untyped) return true; + const objects = contextual.filter((part) => part.flags & ts.TypeFlags.Object); + if (objects.length === 0) return false; + return literal.properties.nodes.some((prop) => { + const name = (prop.name as ts.Identifier | ts.StringLiteral).text; + return objects.every((part) => { + const symbol = this.checker.getPropertyOfType(part, name); + return symbol !== undefined && (symbol.flags & ts.SymbolFlags.Optional) !== 0; + }); + }); + } + /** A TypeScript object literal handed to a JavaScript checked-value * parameter whose inferred type is open (`constructor({ factory, ...options * } = {})` — the rest element admits any property): the literal names a @@ -10652,7 +10697,8 @@ export class Lowerer { builtinOption("UrlObject") || builtinOption("URLFormatOptions") || !this.mapTypeOf(this.typeOf(x)) || - this.excessJsContextualProperty(x, parts) + this.excessJsContextualProperty(x, parts) || + this.reshapeableContextualLiteral(x, parts) ) { return lowerDynObjectLiteral(this, x); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json new file mode 100644 index 000000000..5b1ecde5e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7603-fresh-literal-untyped-param/lib.js", + "/tests/corpus/7603-fresh-literal-untyped-param/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7603-fresh-literal-untyped-param/lib.js b/tests/corpus/7603-fresh-literal-untyped-param/lib.js new file mode 100644 index 000000000..687caf5c2 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/lib.js @@ -0,0 +1,12 @@ +// zod 4's normalizeParams: moves `message` to `error` and deletes it. +export function normalize(params) { + if (params?.message !== undefined) params.error = params.message; + delete params.message; + return params; +} + +export function strip(options) { + for (const key of Object.keys(options)) if (options[key] === undefined) delete options[key]; + options.stripped = true; + return options; +} diff --git a/tests/corpus/7603-fresh-literal-untyped-param/main.ts b/tests/corpus/7603-fresh-literal-untyped-param/main.ts new file mode 100644 index 000000000..c242c7150 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/main.ts @@ -0,0 +1,18 @@ +// A fresh TypeScript object literal handed to an untyped JavaScript +// parameter, or to a parameter type that declares its properties optional, +// is the plain object the callee may reshape: deleting a property works as +// in Node instead of failing on a record layout that requires it. +import { normalize, strip } from "./lib.js"; + +console.log(JSON.stringify(normalize({ message: "must change at least one field" }))); +console.log(JSON.stringify(normalize({ message: "kept", note: 1 }))); +const n = 2; +console.log(JSON.stringify(strip({ a: n, b: undefined, c: "x" }))); + + +// A package-declared parameter whose type makes `message` optional, on a +// method the package's JavaScript defines (zod's `.refine(fn, { message })`). +import { schema } from "params-kit"; +const s = schema("book"); +console.log(s.refine("plain", { message: "must change at least one field" })); +console.log(s.refine("with path", { message: "bad", path: ["a", "b"] })); diff --git a/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts new file mode 100644 index 000000000..608534249 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts @@ -0,0 +1,10 @@ +export interface RefineParams { + message?: string; + error?: unknown; + path?: string[]; +} +export interface Schema { + name: string; + refine(label: string, params?: string | RefineParams): string; +} +export declare function schema(name: string): Schema; diff --git a/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js new file mode 100644 index 000000000..2433d9129 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js @@ -0,0 +1,11 @@ +// zod 4's normalizeParams on an instance method, behind a package declaration. +function normalizeParams(params) { + if (params?.message !== undefined) params.error = params.message; + delete params.message; + return params; +} +export function schema(name) { + const inst = { name }; + inst.refine = (label, params) => `${inst.name}.${label}: ${JSON.stringify(normalizeParams(params))}`; + return inst; +} diff --git a/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json new file mode 100644 index 000000000..1addbb8d0 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json @@ -0,0 +1 @@ +{ "name": "params-kit", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json b/tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json new file mode 100644 index 000000000..62c164e5c --- /dev/null +++ b/tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 3200 } From 1e4889bebeee87710ff37aa565272bc0369d41c6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:44:30 -0500 Subject: [PATCH 115/169] Declare the stream write-with-encoding helper's IrExpr result --- packages/compiler/src/frontend/lowering/lower-stream.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index e35f5564f..c37ae9074 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -5683,7 +5683,10 @@ export function lowerStreamMethodCall( // argument reaches an object-mode side's _write as passed, null when // the callback took its place (a byte-mode side encodes a string // chunk with it). - const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { + const writeEnc = ( + encArg: ts.Expression | undefined, + cbArg: ts.Expression | undefined, + ): IrExpr => { const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); const tail: IrExpr[] = []; @@ -5696,7 +5699,7 @@ export function lowerStreamMethodCall( args: [receiver, numLit(flags, loc), chunk, ...tail], type: BOOL, loc, - } satisfies IrExpr; + }; }; if (kind === "dyn") { if (args.length === 3) return writeEnc(args[1], args[2]); From b161d0be137fa85db1f12ab0e54afc1e1d94f9cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:46:00 -0500 Subject: [PATCH 116/169] Authenticate empty AES-CCM messages in decipher A CCM decipher skipped its one update() when the data was empty, and final() accepted when an update had run without failing, so it never compared the tag. setAuthTag() with any tag of the right length, then update() with empty data, then final(), succeeded: anyone could forge an empty message, or one with different AAD or a different declared plaintextLength, without the key. A decipher's update() now runs the authenticated decryption for empty data too. As in OpenSSL, the tag covers the nonce, the AAD and the length, so the right tag still opens an empty message and any other tag, AAD or length fails in final() with "Unsupported state or unable to authenticate data". final() succeeds only after a tag comparison has succeeded. A cipher's empty update() is unchanged. --- packages/runtime/src/scr_cipher.c | 18 +++++++----- tests/corpus/7155-cipher-aes-ccm.cjs | 41 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 2894af8e2..a2db11434 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -201,6 +201,7 @@ typedef struct ScCipher { bool ccm_iv_set, ccm_len_set, ccm_tag_set; bool ccm_updated; /* a decipher's one update() ran; key wrap: the one update() */ bool ccm_auth_failed; /* that update failed to authenticate */ + bool ccm_verified; /* a decipher's tag matched the one it computed */ double ccm_max_message; bool wrap_decrypt_block; /* key wrap: the AES key schedule decrypts */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ @@ -1284,6 +1285,7 @@ static bool sc_ccm_data(ScCipher *c, const unsigned char *in, unsigned char *out return false; } c->ccm_iv_set = c->ccm_tag_set = c->ccm_len_set = false; + c->ccm_verified = true; return true; } @@ -1294,8 +1296,10 @@ static bool sc_ccm_length_ok(const ScCipher *c, double len) { } /* update(): a decipher takes one, and its authentication failure leaves - * empty output for final() to report. Node passes empty data to OpenSSL - * without a buffer, which the provider skips. */ + * empty output for final() to report. A decipher authenticates even an + * empty message, as OpenSSL does: its tag still covers the nonce, the AAD + * and the length. A cipher's empty update is skipped, and final() then + * runs the empty message. */ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) { if (!sc_ccm_length_ok(c, (double)len)) return NULL; if (c->decrypt && c->ccm_updated) { @@ -1304,7 +1308,7 @@ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) } ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len ? len : 1)); out->is_buffer = true; - bool ok = len == 0 || sc_ccm_data(c, in, out->data, len); + bool ok = (len == 0 && !c->decrypt) || sc_ccm_data(c, len ? in : out->data, out->data, len); if (c->decrypt) { c->ccm_updated = true; if (!ok) c->ccm_auth_failed = true; @@ -1321,11 +1325,11 @@ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) return out; } -/* final(): a decipher reports the update's verdict; a cipher runs the - * provider's final (an empty message when no data was encrypted) and - * reads the tag, which retires the provider's state. */ +/* final(): a decipher succeeds only when its update verified the tag; a + * cipher runs the provider's final (an empty message when no data was + * encrypted) and reads the tag, which retires the provider's state. */ static bool sc_ccm_final(ScCipher *c) { - if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_updated && !c->ccm_auth_failed; + if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_verified; unsigned char dummy = 0, sink = 0; if (c->ccm_iv_set && !c->ccm_tag_set && !sc_ccm_data(c, &dummy, &sink, 0)) return false; if (!c->ccm_tag_set || !sc_ccm_tag(c, c->tag)) return false; diff --git a/tests/corpus/7155-cipher-aes-ccm.cjs b/tests/corpus/7155-cipher-aes-ccm.cjs index 2ed95c382..5e6407e52 100644 --- a/tests/corpus/7155-cipher-aes-ccm.cjs +++ b/tests/corpus/7155-cipher-aes-ccm.cjs @@ -150,6 +150,47 @@ console.log("open empty message", (() => { return JSON.stringify(Buffer.concat([d.update(Buffer.alloc(0)), d.final()]).toString()); })()); +// An empty message is still authenticated: its tag covers the nonce, the +// AAD and the declared length, so a wrong tag, other AAD or another +// plaintextLength fails in final(), as does a decipher without an update. +const empty = Buffer.alloc(0); +const sealEmpty = (aadBuf) => { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + if (aadBuf) c.setAAD(aadBuf, { plaintextLength: 0 }); + c.update(empty); + c.final(); + return c.getAuthTag(); +}; +const emptyTag = sealEmpty(aad); +const emptyTagNoAad = sealEmpty(null); +const flipped = Buffer.from(emptyTag); +flipped[0] ^= 1; +const openEmpty = (steps) => { + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const out = []; + try { + for (const step of steps) { + const v = step(d); + if (Buffer.isBuffer(v)) out.push(v); + } + return `ok ${JSON.stringify(Buffer.concat(out).toString())}`; + } catch (e) { + return `${e.name} ${e.code} ${JSON.stringify(e.message)} after ${out.length} outputs`; + } +}; +console.log("empty tag", emptyTag.toString("hex"), emptyTagNoAad.toString("hex")); +console.log("empty, right tag", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, wrong tag", openEmpty([setAuthTag(Buffer.alloc(16, 9)), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, flipped tag", openEmpty([setAuthTag(flipped), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, other aad", openEmpty([setAuthTag(emptyTag), setAAD(Buffer.from("other"), { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, plaintextLength 5", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 5 }), update(empty), final()])); +console.log("empty, no update", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 0 }), final()])); +console.log("empty, no tag", openEmpty([setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, no aad, right tag", openEmpty([setAuthTag(emptyTagNoAad), update(empty), final()])); +console.log("empty, no aad, wrong tag", openEmpty([setAuthTag(Buffer.alloc(16, 9)), update(empty), final()])); +console.log("empty, aad dropped", openEmpty([setAuthTag(emptyTag), update(empty), final()])); +console.log("empty, a string update", openEmpty([setAuthTag(Buffer.alloc(16, 9)), (d) => d.update(""), final()])); + // As a stream: one write and the end. const stream = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 12 }); const chunks = []; From dd770477b0960ddfb21ec6cd94509950836bb540 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:47:13 -0700 Subject: [PATCH 117/169] Format the boundary harness test Co-Authored-By: Claude Opus 5.5 --- tests/harness/decl-boundary.test.ts | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts index 7f8e93c84..6729dd530 100644 --- a/tests/harness/decl-boundary.test.ts +++ b/tests/harness/decl-boundary.test.ts @@ -17,17 +17,21 @@ const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/lying.ts"); const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/decl-boundary"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -test("boundary casts throw a path TypeError for values the declarations misdescribe", { timeout: 600_000 }, async () => { - mkdirSync(outDir, { recursive: true }); - const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); - if (!result.ok) { - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - } - const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); - pending.child.stdin?.end(); - const { stdout } = await pending; - expect(stdout).toBe( - "liar threw true expected number at $.n, got string\n" + - "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", - ); -}); +test( + "boundary casts throw a path TypeError for values the declarations misdescribe", + { timeout: 600_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + pending.child.stdin?.end(); + const { stdout } = await pending; + expect(stdout).toBe( + "liar threw true expected number at $.n, got string\n" + + "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", + ); + }, +); From e3e955eda084a4599f95f69eba34c2d264d1bd1b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:55:26 -0500 Subject: [PATCH 118/169] Register Math.min and Math.max spreads as generator steps dyn.mathFold steps the iterator of an untyped iterable, so the generator dispatch pass now counts it among the runtime entry points that step iterators. A generator whose only consumer is Math.min or Math.max then has its step hooks registered, and the spread folds its values instead of throwing "Reading this native iterator result has no lowering". --- packages/compiler/src/frontend/lowering/dispatch-sites.ts | 5 +++-- .../tests/corpus/7489-math-spread-generator-only.js.json | 6 ++++++ tests/corpus/7489-math-spread-generator-only.js | 8 ++++++++ .../corpus-costs/7489-math-spread-generator-only.js.json | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json create mode 100644 tests/corpus/7489-math-spread-generator-only.js create mode 100644 tests/harness/corpus-costs/7489-math-spread-generator-only.js.json diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index c8a8d6ba6..f31e5a6ae 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -12,10 +12,11 @@ import { toPrimitiveHint } from "./class-dynamic-dispatch.js"; import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js"; /** Runtime entry points that step a checked-dynamic iterable's iterator: - * destructuring rest, and the collection constructors' and - * Object.fromEntries' seeds. */ + * destructuring rest, the collection constructors' and Object.fromEntries' + * seeds, and Math.min/Math.max over a spread. */ const RUNTIME_STEP_LIB_FNS: ReadonlySet = new Set([ "dyn.destructureRest", + "dyn.mathFold", "dyn.nativeSetNew", "dyn.mapSeedEntries", "dyn.fromEntries", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json new file mode 100644 index 000000000..5277102eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7489-math-spread-generator-only.js" + ], + "diags": [] +} diff --git a/tests/corpus/7489-math-spread-generator-only.js b/tests/corpus/7489-math-spread-generator-only.js new file mode 100644 index 000000000..168057aac --- /dev/null +++ b/tests/corpus/7489-math-spread-generator-only.js @@ -0,0 +1,8 @@ +// Math.max and Math.min as the only consumers of an untyped generator: the +// spread steps the generator, whose values fold with ToNumber as in Node. +function* generate(limit) { + for (let i = 1; i <= limit; i++) yield i % 2 === 0 ? String(i * 3) : i; +} +const lookup = { generate }; +console.log(Math.max(...lookup.generate(4)), Math.min(...lookup.generate(4))); +console.log(Math.max(...lookup.generate(0)), Math.min(...lookup.generate(1))); diff --git a/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json new file mode 100644 index 000000000..2aa0f8038 --- /dev/null +++ b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json @@ -0,0 +1 @@ +{ "plain": 3384, "san": 4732 } From fc31dd50604e73a72a782b0dccc16e2f1643d727 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:56:12 -0500 Subject: [PATCH 119/169] Enumerate each record in the key order its object literal wrote --- docs/content/docs/coverage.mdx | 2 +- .../record-identity-and-property-order.mdx | 7 +- packages/cli/test/native-link-info.test.ts | 4 +- packages/compiler/src/backend/llvm/dyn.ts | 149 ++--- .../backend/llvm/expr-stream-bridges.test.ts | 2 +- .../src/backend/llvm/expr-stream-bridges.ts | 129 ++-- .../compiler/src/backend/llvm/key-orders.ts | 49 ++ packages/compiler/src/backend/llvm/walkers.ts | 139 ++--- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/divergences.ts | 5 + .../lowering/key-order-witness.test.ts | 229 +++++++- .../frontend/lowering/key-order-witness.ts | 549 +++++++++++++++++- .../src/frontend/lowering/lower-calls.ts | 24 +- .../src/frontend/lowering/lower-inspect.ts | 12 +- packages/compiler/src/frontend/type-mapper.ts | 17 +- .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 18 + packages/compiler/test/forin-keys.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../tests/corpus/7610-key-order-json.ts.json | 6 + ...7611-key-order-keys-values-entries.ts.json | 6 + .../corpus/7612-key-order-for-in.ts.json | 6 + .../corpus/7613-key-order-inspect.ts.json | 6 + .../corpus/7614-key-order-copies.ts.json | 6 + .../corpus/7615-key-order-dynamic.ts.json | 6 + .../corpus/7616-key-order-late-writes.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 6 + packages/runtime/src/scr_json.c | 3 +- tests/corpus/7610-key-order-json.ts | 31 + .../7611-key-order-keys-values-entries.ts | 19 + tests/corpus/7612-key-order-for-in.ts | 15 + tests/corpus/7613-key-order-inspect.ts | 12 + tests/corpus/7614-key-order-copies.ts | 32 + tests/corpus/7615-key-order-dynamic.ts | 30 + tests/corpus/7616-key-order-late-writes.ts | 44 ++ tests/coverage-fixtures/divergences.ts | 10 +- .../__snapshots__/coverage-divergences.txt | 10 +- .../corpus-costs/7610-key-order-json.ts.json | 1 + ...7611-key-order-keys-values-entries.ts.json | 1 + .../7612-key-order-for-in.ts.json | 1 + .../7613-key-order-inspect.ts.json | 1 + .../7614-key-order-copies.ts.json | 1 + .../7615-key-order-dynamic.ts.json | 1 + .../7616-key-order-late-writes.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 51 files changed, 1397 insertions(+), 224 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/key-orders.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json create mode 100644 tests/corpus/7610-key-order-json.ts create mode 100644 tests/corpus/7611-key-order-keys-values-entries.ts create mode 100644 tests/corpus/7612-key-order-for-in.ts create mode 100644 tests/corpus/7613-key-order-inspect.ts create mode 100644 tests/corpus/7614-key-order-copies.ts create mode 100644 tests/corpus/7615-key-order-dynamic.ts create mode 100644 tests/corpus/7616-key-order-late-writes.ts create mode 100644 tests/harness/corpus-costs/7610-key-order-json.ts.json create mode 100644 tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json create mode 100644 tests/harness/corpus-costs/7612-key-order-for-in.ts.json create mode 100644 tests/harness/corpus-costs/7613-key-order-inspect.ts.json create mode 100644 tests/harness/corpus-costs/7614-key-order-copies.ts.json create mode 100644 tests/harness/corpus-costs/7615-key-order-dynamic.ts.json create mode 100644 tests/harness/corpus-costs/7616-key-order-late-writes.ts.json diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 954b040db..db1193b98 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -97,7 +97,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them. - `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity. -- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. +- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 0381803f6..cc16f74d5 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -10,9 +10,10 @@ A type whose values are only read field by field, from fields the class does not Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference. -`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. -The order matches Node.js when every value of the type is created in the same order. -When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js. +scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. +When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last. +A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders. A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index bf5b3be14..8715d9356 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v33"); + expect(error?.stderr).toContain("scr_runtime_abi_v34"); }); }); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..04e3b9817 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,3 +1,4 @@ +import { emitPerKeyOrder } from "./key-orders.js"; import { typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js"; @@ -37,6 +38,8 @@ import { accessorSlotProp, DYN_HANDLE_KINDS, isDirentShape, + KEY_ORDER_FIELD, + shapeKeyOrders, isDynTypedRefType, isRefCounted, isUnitType, @@ -1107,9 +1110,10 @@ export class LlDyn { } B.condBr(readable, l0, fail); B.startBlock(l0); - // dyn ('unknown') fields match ANY value, present or missing. + // dyn ('unknown') fields match ANY value, present or missing. The + // order index is no key. for (const f of shape.fields) { - if (f.type.kind === "dyn") continue; + if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); const lNext = B.newLabel("dm.n"); this.pendingBail( @@ -1292,9 +1296,10 @@ export class LlDyn { return ( shape.fields.every( (f) => - !f.name.startsWith("%") && - f.name !== "__proto__" && - this.jsonSchemaSupported(f.type, visiting, true), + f.name === KEY_ORDER_FIELD || + (!f.name.startsWith("%") && + f.name !== "__proto__" && + this.jsonSchemaSupported(f.type, visiting, true)), ) && (!index || this.jsonSchemaSupported(index, visiting)) ); @@ -1380,7 +1385,9 @@ export class LlDyn { const base = host.nullableUnions.fieldBase(t.shapeId); const index = new Map(shape.fields.map((f, i) => [f.name, i + base])); // The runtime names kept-source keys by this index (record-sources.ts). - const order = recordSourceFieldOrder(shape); + // The order index is no key: parsed values keep the table's order + // and their source's own order. + const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD); if (recordMayKeepSource(shape)) this.keepsRecordSources = true; const fieldTy = `{ ptr, ${S}, ${S}, ptr }`; const entries = order.map((fieldName) => { @@ -2156,7 +2163,10 @@ export class LlDyn { } B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`); if (recursive) this.recordBuilt(B, name, "%r0"); + // The order index stays 0: the source's own order is kept below + // when it differs from the table. for (const f of shape.fields) { + if (f.name === KEY_ORDER_FIELD) continue; const { value: m, release } = this.memberRead(B, f.name, f.type); this.pendingBail( B, @@ -2889,70 +2899,79 @@ export class LlDyn { host.declare(`declare ptr @scr_dyn_new_obj()`); B.line(`${d} = call ptr @scr_dyn_new_obj()`); } - // Keys insert in DECLARED order (JS insertion order); internal - // '%'-fields follow so a record→dyn→record round trip keeps them. + // Keys insert in DECLARED order (JS insertion order), or in the + // value's own order for a shape with orderVariants; internal + // '%'-fields follow so a record→dyn→record round trip keeps them + // (all but the order index, which is no key). const byName = new Map(shape.fields.map((f) => [f.name, f])); // An accessor property is one key whose getter and setter are the // slot closures (see accessorSlotProp), in accessorOrder's place. - const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - const accessors = new Set( - shape.accessorOrder === undefined - ? [] - : shape.fields.flatMap((f) => { - const slot = accessorSlotProp(f.name); - return slot === null ? [] : [slot.prop]; - }), - ); - type DynField = { name: string; type: IrType } | { accessor: string }; - const dynFields: DynField[] = [ - ...order.flatMap((n) => { - if (accessors.has(n)) return [{ accessor: n } as DynField]; - const f = byName.get(n); - return f === undefined ? [] : [f as DynField]; - }), - ...shape.fields.filter( - (f) => - !inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null), - ), - ]; - for (const entry of dynFields) { - if ("accessor" in entry) { - host.declare( - `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, - ); - const boxed = (kind: "get" | "set"): string => { - const slot = byName.get(`%${kind}:${entry.accessor}`); - if (slot === undefined) return "null"; - const fv = loadFieldOf(slot.name, slot.type); - const conv = B.tmp(); - B.line( - `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, - ); - return conv; - }; - const getter = boxed("get"); - const setter = boxed("set"); - B.line( - `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, - ); - continue; - } - const f = entry; - const klen = Buffer.byteLength(f.name, "utf8"); - const fv = loadFieldOf(f.name, f.type); - // An ABSENT optional field contributes no key. - const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; - const conv = B.tmp(); - B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); - B.line( - `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + const orders = + shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape); + const perOrder = (order: readonly string[]): void => { + const inOrder = new Set(order); + const accessors = new Set( + shape.accessorOrder === undefined + ? [] + : shape.fields.flatMap((f) => { + const slot = accessorSlotProp(f.name); + return slot === null ? [] : [slot.prop]; + }), ); - if (lSkip !== null) { - B.br(lSkip); - B.startBlock(lSkip); + type DynField = { name: string; type: IrType } | { accessor: string }; + const dynFields: DynField[] = [ + ...order.flatMap((n) => { + if (accessors.has(n)) return [{ accessor: n } as DynField]; + const f = byName.get(n); + return f === undefined ? [] : [f as DynField]; + }), + ...shape.fields.filter( + (f) => + !inOrder.has(f.name) && + f.name !== KEY_ORDER_FIELD && + !(accessors.size > 0 && accessorSlotProp(f.name) !== null), + ), + ]; + for (const entry of dynFields) { + if ("accessor" in entry) { + host.declare( + `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, + ); + const boxed = (kind: "get" | "set"): string => { + const slot = byName.get(`%${kind}:${entry.accessor}`); + if (slot === undefined) return "null"; + const fv = loadFieldOf(slot.name, slot.type); + const conv = B.tmp(); + B.line( + `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + ); + return conv; + }; + const getter = boxed("get"); + const setter = boxed("set"); + B.line( + `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, + ); + continue; + } + const f = entry; + const klen = Buffer.byteLength(f.name, "utf8"); + const fv = loadFieldOf(f.name, f.type); + // An ABSENT optional field contributes no key. + const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null; + const conv = B.tmp(); + B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`); + B.line( + `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, + ); + if (lSkip !== null) { + B.br(lSkip); + B.startBlock(lSkip); + } } - } + }; + if (orders.length === 1) perOrder(orders[0]!); + else emitPerKeyOrder(B, shape, base, "%v", perOrder); // A Dirent row takes Node's Dirent prototype (the type probes) and // its Symbol(type) key; %dtype stays readable but non-enumerable. if (isDirentShape(shape)) { diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts index 5d8f5f26f..244b3afea 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts @@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => { const llvm = emitLlvmModule(module, { pointerBits: 64 }); const tables = [ ...llvm.matchAll( - /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{ i64 (\d+), /gm, + /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm, ), ]; // root and leaf get field hooks; the record with an index-like field name diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 7cb0fa772..ceaca3778 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -13,6 +13,8 @@ import { classFieldAttributesName, isClassOwnEnumerableFieldName, isDirentShape, + KEY_ORDER_FIELD, + shapeKeyOrders, isDynTypedRefType, isRefCounted, shapeHasAccessorSlots, @@ -44,6 +46,7 @@ import type { NullableRefField } from "./nullable-fields.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { inlineUnionCopyLines } from "./inline-unions.js"; import { recordMayKeepSource } from "./record-sources.js"; +import { emitPerKeyOrder } from "./key-orders.js"; import { registerReplay } from "./emit-journal.js"; /** An owned (+1) union value for a nullable-pointer class field slot `p` @@ -1065,52 +1068,68 @@ export function streamTypedRefMaterializeAdapter( const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_obj()`); const byName = new Map(shape.fields.map((field) => [field.name, field])); - const order = shape.declaredOrder ?? shape.fields.map((field) => field.name); - const inOrder = new Set(order); - const fields = [ - ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), - ...shape.fields.filter((field) => !inOrder.has(field.name)), - ]; + // A shape with orderVariants has one field list per key order; its + // order index is no key. + const orders = shapeKeyOrders(shape); + const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => { + const inOrder = new Set(order); + return [ + ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), + ...shape.fields.filter( + (field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD, + ), + ]; + }; // Published before the field converters, like the array hooks below. // A Dirent row reads through its finished view (the prototype and // the non-enumerable %dtype). const dirent = isDirentShape(shape); - if (!dirent && liveRecordOpsEligible(fields)) { - adapter.recordOps = `@${snapshot}_rops`; - liveRecordOps(host, t, snapshot, fields); - } - for (const field of fields) { - const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); - const fieldPtr = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, - ); - const fieldValue = loadStructField( - B, - host.sizeType, - fieldPtr, - field.type, - host.inlineUnions.record(field.type), - ); - // An ABSENT optional field contributes no key. - const undefinedTag = undefinedArmTag(field.type, host.unionsById); - let skip: string | null = null; - if (undefinedTag >= 0 && field.type.kind === "union") { - const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); - skip = B.newLabel("live.record.absent"); - const set = B.newLabel("live.record.set"); - B.condBr(absent, skip, set); - B.startBlock(set); - } - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); - B.line( - `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); + if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) { + if (orders.length === 1) { + adapter.recordOps = `@${snapshot}_rops`; + liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!)); + } else { + orders.forEach((order, i) => + liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)), + ); + adapter.recordOps = `@${snapshot}_rops`; + liveRecordOrderSelect(host, shape, snapshot); } } + emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => { + for (const field of fieldsOf(order)) { + const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); + const fieldPtr = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`, + ); + const fieldValue = loadStructField( + B, + host.sizeType, + fieldPtr, + field.type, + host.inlineUnions.record(field.type), + ); + // An ABSENT optional field contributes no key. + const undefinedTag = undefinedArmTag(field.type, host.unionsById); + let skip: string | null = null; + if (undefinedTag >= 0 && field.type.kind === "union") { + const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId); + skip = B.newLabel("live.record.absent"); + const set = B.newLabel("live.record.set"); + B.condBr(absent, skip, set); + B.startBlock(set); + } + const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + B.line( + `call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); + } + } + }); if (dirent) { host.declare(`declare void @scr_dyn_dirent_finish(ptr)`); B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`); @@ -1373,7 +1392,35 @@ function liveRecordOps( ``, `@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`, // Mutable: the runtime fills the trailing key-array slot on first use. - `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`, + `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`, + ``, + ); +} + +/** `@_rops` for a shape with orderVariants: a record-ops table + * whose `select` hook answers the `_o_rops` table (liveRecordOps) of the + * record's current key order, which a write of an optional key can change + * after the record is boxed. */ +function liveRecordOrderSelect( + host: LlvmEmitterContext, + shape: IrRecordShape, + snapshot: string, +): void { + const B = new BlockBuilder(); + const out = B.slot(); + B.entryAllocas.push(`${out} = alloca ptr`); + emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => { + B.line(`store ptr @${snapshot}_o${i}_rops, ptr ${out}`); + }); + const ops = B.tmp(); + B.line(`${ops} = load ptr, ptr ${out}`); + B.terminate(`ret ptr ${ops}`); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_select(ptr %p) ${FN_ATTRS} { ; live record ops by key order`, + B.render(), + `}`, + ``, + `@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`, ``, ); } diff --git a/packages/compiler/src/backend/llvm/key-orders.ts b/packages/compiler/src/backend/llvm/key-orders.ts new file mode 100644 index 000000000..9dbda1414 --- /dev/null +++ b/packages/compiler/src/backend/llvm/key-orders.ts @@ -0,0 +1,49 @@ +/* Key-order dispatch for shapes whose values record the order their keys + * were written in (IrRecordShape.orderVariants): an emitter that walks a + * record's keys emits one walk per order and branches on the value's + * KEY_ORDER_FIELD. */ +import type { IrRecordShape } from "../../ir/ir.js"; +import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js"; +import { mangleRecordStruct } from "../mangle.js"; +import type { BlockBuilder } from "./blocks.js"; + +/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a + * switch on the value's order index when the shape has several. Each walk + * starts in its own block, and the code after this call continues in the + * block they all branch to. */ +export function emitPerKeyOrder( + B: BlockBuilder, + shape: IrRecordShape, + fieldBase: number, + rec: string, + walk: (order: readonly string[], index: number) => void, +): void { + const orders = shapeKeyOrders(shape); + if (orders.length === 1) { + walk(orders[0]!, 0); + return; + } + const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase; + const p = B.tmp(); + const n = B.tmp(); + const i = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`, + ); + B.line(`${n} = load double, ptr ${p}`); + B.line(`${i} = fptosi double ${n} to i32`); + const labels = orders.map(() => B.newLabel("ko")); + const join = B.newLabel("ko.join"); + B.terminate( + `switch i32 ${i}, label %${labels[0]} [ ${labels + .slice(1) + .map((label, k) => `i32 ${k + 1}, label %${label}`) + .join(" ")} ]`, + ); + orders.forEach((order, k) => { + B.startBlock(labels[k]!); + walk(order, k); + B.br(join); + }); + B.startBlock(join); +} diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 044cb79ae..331ea2199 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -1,3 +1,4 @@ +import { emitPerKeyOrder } from "./key-orders.js"; import { f64Lit } from "./common.js"; import { InternalCompilerError } from "../../errors.js"; /* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror @@ -328,78 +329,80 @@ export class LlWalkers { if (cyclic) this.jbLeave(B); return; } - // Fields serialize in DECLARED order (JS insertion order); internal + // Fields serialize in DECLARED order (JS insertion order), or in the + // value's own order for a shape with orderVariants; internal // '%'-fields stay hidden — jsonWriteHelper's contract, ported comment // and all. - const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { - throw new InternalCompilerError( - `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, - ); - } - const byName = new Map(shape.fields.map((f) => [f.name, f])); - // Function-valued properties are omitted, as JSON.stringify omits them. - const emitFields = order - .map((n) => byName.get(n)) - .filter((f) => f !== undefined) - .filter((f) => f.type.kind !== "func"); - const droppable = - emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || - !!shape.indexValue; - this.putc(B, "%b", "123"); // '{' - if (!droppable) { - emitFields.forEach((f, i) => { - this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => { + const inOrder = new Set(order); + if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { + throw new InternalCompilerError( + `llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`, ); - }); - } else { - const first = B.slot(); - B.entryAllocas.push(`${first} = alloca i1`); - B.line(`store i1 true, ptr ${first}`); - const comma = (): void => { - const isf = B.tmp(); - const lc = B.newLabel("jwc.c"); - const lj = B.newLabel("jwc.j"); - B.line(`${isf} = load i1, ptr ${first}`); - B.condBr(isf, lj, lc); - B.startBlock(lc); - this.putc(B, "%b", "44"); // ',' - B.br(lj); - B.startBlock(lj); - B.line(`store i1 false, ptr ${first}`); - }; - for (const f of emitFields) { - const utag = undefinedArmTag(f.type, this.host.unionsById); - const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); - let skip: string | null = null; - if (utag >= 0 && f.type.kind === "union") { - // Undefined-valued field: dropped, like Node. - const tag = this.unionTag(B, v, f.type.unionId); - const isu = B.tmp(); - B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); - const lw = B.newLabel("jwf.w"); - skip = B.newLabel("jwf.s"); - B.condBr(isu, skip, lw); - B.startBlock(lw); - } - comma(); - this.puts(B, "%b", jsonObjectKeyLabel(f.name)); - if (edgeable(f.type)) this.jbEdgeProp(B, f.name); - B.line( - `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, - ); - if (skip !== null) { - B.br(skip); - B.startBlock(skip); - } } - if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); - } + const byName = new Map(shape.fields.map((f) => [f.name, f])); + // Function-valued properties are omitted, as JSON.stringify omits them. + const emitFields = order + .map((n) => byName.get(n)) + .filter((f) => f !== undefined) + .filter((f) => f.type.kind !== "func"); + const droppable = + emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) || + !!shape.indexValue; + this.putc(B, "%b", "123"); // '{' + if (!droppable) { + emitFields.forEach((f, i) => { + this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + }); + } else { + const first = B.slot(); + B.entryAllocas.push(`${first} = alloca i1`); + B.line(`store i1 true, ptr ${first}`); + const comma = (): void => { + const isf = B.tmp(); + const lc = B.newLabel("jwc.c"); + const lj = B.newLabel("jwc.j"); + B.line(`${isf} = load i1, ptr ${first}`); + B.condBr(isf, lj, lc); + B.startBlock(lc); + this.putc(B, "%b", "44"); // ',' + B.br(lj); + B.startBlock(lj); + B.line(`store i1 false, ptr ${first}`); + }; + for (const f of emitFields) { + const utag = undefinedArmTag(f.type, this.host.unionsById); + const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type); + let skip: string | null = null; + if (utag >= 0 && f.type.kind === "union") { + // Undefined-valued field: dropped, like Node. + const tag = this.unionTag(B, v, f.type.unionId); + const isu = B.tmp(); + B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`); + const lw = B.newLabel("jwf.w"); + skip = B.newLabel("jwf.s"); + B.condBr(isu, skip, lw); + B.startBlock(lw); + } + comma(); + this.puts(B, "%b", jsonObjectKeyLabel(f.name)); + if (edgeable(f.type)) this.jbEdgeProp(B, f.name); + B.line( + `call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`, + ); + if (skip !== null) { + B.br(skip); + B.startBlock(skip); + } + } + if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue)); + } + }); this.putc(B, "%b", "125"); // '}' if (cyclic) this.jbLeave(B); } diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 7d3dcbc02..173de6408 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 33 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const; +export const RUNTIME_ABI_VERSION = 34 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v34" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 532051b88..923b55c5b 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 18b04b9a6..681f86817 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index c971c672d..87169539f 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -727,6 +727,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { + // Values of a shape with orderVariants enumerate in the order their + // literal wrote (key-order-witness.ts). + const mapped = safely(() => lowerer.mapTypeOf(contextual)); + if (mapped?.kind === "record" && lowerer.shapes.get(mapped.shapeId)?.orderVariants !== undefined) + return null; // A shape whose table the witness pass re-chose (key-order-witness.ts) // enumerates that table, not the checker's declaration order. const table = witnessTable(lowerer, contextual); 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 8632bad47..41398a1de 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -1,5 +1,12 @@ import { expect, test } from "vitest"; -import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; +import { + F64, + KEY_ORDER_FIELD, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js"; import { ShapeRegistry } from "../type-mapper.js"; import { @@ -21,7 +28,7 @@ interface Fixture { notes: KeyOrderNotes; record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; - run(body: IrStmt[]): void; + run(body: IrStmt[]): IrFunction; order(type: IrType & { kind: "record" }): string[] | undefined; } @@ -35,6 +42,14 @@ function fixture(): Fixture { classes: new Map(), armTag: (unionId: string, arm: IrType): number => unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, + recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({ + kind: "recordHas", + obj, + shapeId, + field, + type: { kind: "bool" }, + loc, + }), } as unknown as Lowerer; const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ kind: "record", @@ -55,15 +70,20 @@ function fixture(): Fixture { segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), ); }; - const run = (body: IrStmt[]): void => { + const run = (body: IrStmt[]): IrFunction => { const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; applyKeyOrderWitnesses(lowerer, [fn]); + return fn; }; const order = (type: IrType & { kind: "record" }): string[] | undefined => shapes.get(type.shapeId)?.declaredOrder; return { lowerer, shapes, notes, record, literal, run, order }; } +/** The order index a literal stores (KEY_ORDER_FIELD), when it stores one. */ +const orderIndexOf = (lit: IrExpr): IrExpr | undefined => + lit.kind === "recordLit" ? lit.fields.find((x) => x.name === KEY_ORDER_FIELD)?.value : undefined; + const observe = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, @@ -109,7 +129,7 @@ test("literals that agree with the table leave the same array in place", () => { expect(f.notes.changed.size).toBe(0); }); -test("values built in two orders keep today's table", () => { +test("values built in two orders each record their literal's order", () => { const f = fixture(); const ab = f.record( [ @@ -118,8 +138,71 @@ test("values built in two orders keep today's table", () => { ], ["a", "b"], ); - f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); + const ba = f.literal(ab, ["b", "a"]); + const aOrder = f.literal(ab, ["a", "b"]); + f.run([observe(ba), observe(aOrder)]); expect(f.order(ab)).toEqual(["a", "b"]); + const shape = f.shapes.get(ab.shapeId)!; + expect(shape.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); + expect(shape.fields.map((x) => x.name)).toEqual([KEY_ORDER_FIELD, "a", "b"]); + expect(orderIndexOf(ba)).toMatchObject({ kind: "numLit", value: 1 }); + expect(orderIndexOf(aOrder)).toMatchObject({ kind: "numLit", value: 0 }); +}); + +test("a copy of a value with orders stores its source's index", () => { + const f = fixture(); + const abc = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["a", "b", "c"], + ); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const read = (field: string): IrExpr => ({ + kind: "recordGet", + obj: ref("x", abc), + shapeId: abc.shapeId, + field, + type: F64, + loc, + }); + const rest = f.notes.copy( + { + kind: "recordLit", + fields: [ + { name: "a", value: read("a") }, + { name: "b", value: read("b") }, + ], + type: ab, + loc, + }, + abc.shapeId, + ); + f.run([ + decl("x", f.literal(abc, ["c", "b", "a"])), + observe(f.literal(abc, ["a", "b", "c"])), + observe(rest), + ]); + expect(f.shapes.get(ab.shapeId)!.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); + expect(orderIndexOf(rest)).toMatchObject({ + kind: "recordGet", + obj: { kind: "varRef", localId: "x" }, + field: KEY_ORDER_FIELD, + }); }); test("an unobserved shape keeps today's table", () => { @@ -337,7 +420,7 @@ test("straight-line writes of late-only keys keep program order", () => { expect(f.order(o)).toEqual(["id", "y", "x"]); }); -test("late-only keys written through different bindings keep today's table", () => { +test("late-only keys written through different bindings move to the end per value", () => { const f = fixture(); const o = f.record( [ @@ -354,9 +437,13 @@ test("late-only keys written through different bindings keep today's table", () observe(ref("o", o)), ]); expect(f.order(o)).toEqual(["id", "x", "y"]); + expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ + ["id", "x", "y"], + ["id", "y", "x"], + ]); }); -test("a late-only key written in a loop over an outer binding keeps today's table", () => { +test("a late-only key written in a loop guards the write with its move", () => { const f = fixture(); const o = f.record( [ @@ -366,7 +453,7 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl ], ["id", "x", "y"], ); - f.run([ + const body: IrStmt[] = [ decl("o", f.literal(o, ["id"])), set(ref("o", o), "y"), { @@ -376,11 +463,18 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl loc, }, observe(ref("o", o)), - ]); + ]; + const fn = f.run(body); expect(f.order(o)).toEqual(["id", "x", "y"]); + const loop = fn.body.find((st) => st.kind === "while") as IrStmt & { kind: "while" }; + const guarded = loop.body[0]!; + expect(guarded.kind).toBe("block"); + const [move, write] = (guarded as IrStmt & { kind: "block" }).body; + expect(move).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); + expect(write).toMatchObject({ kind: "recordSet", field: "x" }); }); -test("re-adding a key a literal creates earlier keeps today's table", () => { +test("re-adding a key a literal creates earlier moves it to the end of that value", () => { const f = fixture(); const o = f.record( [ @@ -399,4 +493,119 @@ test("re-adding a key a literal creates earlier keeps today's table", () => { }; f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]); expect(f.order(o)).toEqual(["a", "b"]); + expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([ + ["a", "b"], + ["b", "a"], + ]); +}); + +test("a moving write evaluates its value before it tests and moves the key", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + // `o.x = ((o.y = 2), 3)`: y is created first. + const value: IrExpr = { + kind: "seqExpr", + stmts: [set(ref("o", o), "y")], + result: num(3), + type: F64, + loc, + }; + const write: IrStmt = { + kind: "recordSet", + obj: ref("o", o), + shapeId: o.shapeId, + field: "x", + value, + loc, + }; + const fn = f.run([ + decl("o", f.literal(o, ["id"])), + observe(f.literal(o, ["x", "id"])), + write, + observe(ref("o", o)), + ]); + const shape = f.shapes.get(o.shapeId)!; + expect(shape.orderVariants).toBeDefined(); + const block = fn.body[2] as IrStmt & { kind: "block" }; + expect(block.kind).toBe("block"); + // The receiver evaluates first, since the value may assign its binding. + const [receiver, first, guard, written] = block.body; + expect(receiver).toMatchObject({ kind: "varDecl", init: { kind: "varRef", localId: "o" } }); + const target = (receiver as IrStmt & { kind: "varDecl" }).localId; + expect(first).toMatchObject({ kind: "varDecl", init: { kind: "seqExpr" } }); + const local = (first as IrStmt & { kind: "varDecl" }).localId; + expect(guard).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } }); + expect(written).toMatchObject({ + kind: "recordSet", + obj: { kind: "varRef", localId: target }, + field: "x", + value: { kind: "varRef", localId: local }, + }); + // The write inside the value moves y in its own block. + const init = (first as IrStmt & { kind: "varDecl" }).init as IrExpr & { kind: "seqExpr" }; + expect(init.stmts[0]).toMatchObject({ kind: "block" }); +}); + +test("a key after a spread that the source may lack picks its order from the source's keys", () => { + const f = fixture(); + const s = f.record( + [ + ["a", F64], + ["b", OPT], + ["c", OPT], + ], + ["a", "b", "c"], + ); + const t = f.record( + [ + ["a", F64], + ["b", F64], + ["c", OPT], + ], + ["a", "b", "c"], + ); + const read = (field: string): IrExpr => ({ + kind: "recordGet", + obj: ref("x", s), + shapeId: s.shapeId, + field, + type: OPT, + loc, + }); + // `{ ...x, b: 1 }` + const merged = f.notes.literal( + { + kind: "recordLit", + fields: [ + { name: "a", value: read("a") }, + { name: "c", value: read("c") }, + { name: "b", value: num(1) }, + ], + type: t, + loc, + }, + [ + { kind: "spread", shapeId: s.shapeId, maybeAbsent: false }, + { kind: "key", name: "b", definite: true }, + ], + ); + f.run([decl("x", f.literal(s, ["c", "a"])), observe(f.literal(s, ["a", "c"])), observe(merged)]); + // b keeps its place in the source's order when the source has it, and + // follows the source's keys when it does not. + expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([ + ["a", "c", "b"], + ["c", "a", "b"], + ["a", "b", "c"], + ]); + expect(orderIndexOf(merged)).toMatchObject({ + kind: "ternary", + cond: { kind: "recordHas", obj: { kind: "varRef", localId: "x" }, field: "b" }, + }); }); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 4adfb87ab..728a2296e 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -41,12 +41,57 @@ * value: that shape, and every shape copied from it, keeps today's table * (and today's SC6002 warnings). Shapes that no order surface observes * keep today's table too. */ -import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js"; -import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; -import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; +import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; +import { + BOOL, + F64, + KEY_ORDER_FIELD, + shapeHasAccessorSlots, + typeEquals, + UNDEFINED_T, +} from "../../ir/ir.js"; +import { + everyExprChild, + everyStmtChild, + everyTypeChild, + transformStmtList, +} from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.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 + * branch per order (walks[i] for order i) on the value's KEY_ORDER_FIELD. + * `walks` follows shapeKeyOrders. */ +export function perKeyOrder( + shape: IrRecordShape, + ref: IrExpr, + walks: readonly IrStmt[][], + loc: SrcLoc, +): IrStmt[] { + let tail = walks[0]!; + for (let i = walks.length - 1; i >= 1; i--) { + const index: IrExpr = { + kind: "recordGet", + obj: ref, + shapeId: shape.id, + field: KEY_ORDER_FIELD, + type: F64, + loc, + }; + const cond: IrExpr = { + kind: "bin", + op: "===", + left: index, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }; + tail = [{ kind: "if", cond, then: walks[i]!, else_: tail, loc }]; + } + return tail; +} + /** One property of an object literal, in source order. */ export type KeyOrderSegment = /** An explicit key (`definite`), or a conditional spread's key, which @@ -329,6 +374,10 @@ 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": + case "promiseWithResolvers": producedBy(e.type, true); return; case "dynCheck": @@ -713,4 +762,498 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; notes.changed.add(id); } + + // ── 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 + // in (KEY_ORDER_FIELD), when every construction's order is known + // statically or copied from a value that records its own: + // - a literal of explicit keys writes its order; + // - a copy from a shape with one resolved order writes that order; + // - a copy from a value of a shape with orders writes the source's + // order, filtered to the copy's keys (rest bindings, width copies); + // - clones copy the field, and every other construction (synthesized + // literals, checked conversions, runtime producers) leaves 0, today's + // table, as before. + // A write of an optional key a value lacks moves the key to the end of + // the value's order, as in Node: the orders include every order such + // writes reach, and each write site updates the index after it evaluates + // the value and before it writes the key. + const MAX_ORDERS = 16; + const unresolved = (id: string): boolean => result.get(id) === "unknown"; + /** `order` over every key of `table`: keys the order lacks follow it. */ + const complete = (order: readonly string[], table: readonly string[]): string[] => { + const keys = new Set(table); + const out = order.filter((k) => keys.has(k)); + const seen = new Set(out); + return [...out, ...table.filter((k) => !seen.has(k))]; + }; + const literalOrder = (segments: readonly KeyOrderSegment[]): string[] | null => { + const out: string[] = []; + for (const segment of segments) { + if (segment.kind !== "key" || !segment.definite) return null; + if (!isArrayIndex(segment.name)) out.push(segment.name); + } + return out; + }; + /** `{ ...value }` of a value of the literal's own shape: a copy of the + * value's order. */ + const selfSpread = (segments: readonly KeyOrderSegment[], id: string): boolean => + segments.length === 1 && + segments[0]!.kind === "spread" && + segments[0]!.shapeId === id && + !segments[0]!.maybeAbsent; + const sameOrder = (a: readonly string[], b: readonly string[]): boolean => + a.join("\0") === b.join("\0"); + const indexOfOrder = (orders: readonly string[][], order: readonly string[]): number => + orders.findIndex((o) => sameOrder(o, order)); + /** The keys writes can add to a value of the shape, or null when a + * clone override can (the clone's order is its source's). */ + const movingKeys = (id: string, f: ShapeFacts, table: readonly string[]): string[] | null => { + const keys = new Set(table); + const moving = new Set(); + for (const w of f.writes) { + if (w.kind === "delete" || !keys.has(w.field) || !maybeAbsent(id, w.field)) continue; + if (w.kind === "clone") return null; + moving.add(w.field); + } + return [...moving]; + }; + /** `values[i]` for the order index `current` holds, as a chain of + * tests. */ + const indexChain = (current: IrExpr, values: readonly number[], loc: SrcLoc): IrExpr => { + let out: IrExpr = { kind: "numLit", value: values[values.length - 1]!, type: F64, loc }; + for (let i = values.length - 2; i >= 0; i--) { + if (out.kind === "numLit" && out.value === values[i]) continue; + out = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: current, + right: { kind: "numLit", value: i, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: values[i]!, type: F64, loc }, + else_: out, + type: F64, + loc, + }; + } + return out; + }; + const moves = new Map>(); + /** Adds to `orders` every order writes of `moving` keys reach from them + * and records each key's transitions; false past MAX_ORDERS. */ + const closeUnderMoves = (id: string, orders: string[][], moving: readonly string[]): boolean => { + const transitions = new Map(moving.map((k) => [k, []])); + for (let i = 0; i < orders.length; i++) { + for (const key of moving) { + const moved = [...orders[i]!.filter((k) => k !== key), key]; + let index = indexOfOrder(orders, moved); + if (index < 0) { + if (orders.length === MAX_ORDERS) return false; + index = orders.push(moved) - 1; + } + transitions.get(key)![i] = index; + } + } + if (moving.length > 0) moves.set(id, transitions); + return true; + }; + // Observed shapes, and the shapes their values copy their orders from. + const wanted = new Set(observed); + const pending = [...observed]; + while (pending.length > 0) { + const f = facts.get(pending.pop()!); + if (f === undefined) continue; + const sources = [ + ...f.copies.map((c) => c.source), + ...f.literals.flatMap((segments) => + segments.flatMap((s) => (s.kind === "spread" ? [s.shapeId] : [])), + ), + ]; + for (const source of sources) { + if (wanted.has(source)) continue; + wanted.add(source); + pending.push(source); + } + } + const orderable = ( + id: string, + f: ShapeFacts, + ): { shape: IrRecordShape; moving: string[] } | null => { + const shape = shapes.get(id); + if (!wanted.has(id) || !unresolved(id) || !inScope(shape)) return null; + const moving = movingKeys(id, f, tableOf(shape)); + return moving === null ? null : { shape, moving }; + }; + const variants = new Map(); + for (const [id, f] of facts) { + const candidate = orderable(id, f); + // A runtime producer builds the shape's values in its fixed layout, + // which has no KEY_ORDER_FIELD. + if (candidate === null || f.literals.length === 0 || f.opaque) continue; + const table = tableOf(candidate.shape); + const orders: string[][] = [table]; + let ok = true; + const add = (order: readonly string[]): void => { + const full = complete(order, table); + if (indexOfOrder(orders, full) < 0) orders.push(full); + }; + for (const segments of f.literals) { + if (selfSpread(segments, id)) continue; + const order = literalOrder(segments); + if (order === null) ok = false; + else add(order); + } + for (const c of f.copies) { + if (c.source === id) continue; + const source = result.get(c.source); + if (source === undefined || source === "unknown") ok = false; + else add(source.order.filter((k) => c.keys.has(k))); + } + if (!ok || orders.length > MAX_ORDERS || !closeUnderMoves(id, orders, candidate.moving)) + continue; + if (orders.length > 1) variants.set(id, orders); + else moves.delete(id); + } + // Shapes built only from values of one shape with orders, all in one + // way: copies (rest bindings, width copies), or literals spreading the + // value between the same explicit keys. Order i is the source's order i + // over the shape's keys, after the keys before the spread and before + // the keys after it. A key after the spread that the source value may + // lack goes where the source has it, or after the source's keys when it + // has not: such a literal's orders are one per source order and set of + // those keys the source has, and `select.index[present][i]` is the order + // for source order i with the keys of bit set `present`. + type Template = { + source: string; + prefix: string[]; + suffix: string[]; + select?: { keys: string[]; index: number[][] }; + }; + const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => { + const prefix: string[] = []; + const suffix: string[] = []; + let source: string | null = null; + for (const segment of segments) { + if (segment.kind === "opaque") return null; + if (segment.kind === "spread") { + if (source !== null || segment.maybeAbsent || segment.shapeId === id) return null; + source = segment.shapeId; + continue; + } + if (!segment.definite) return null; + (source === null ? prefix : suffix).push(segment.name); + } + return source === null ? null : { source, prefix, suffix }; + }; + const templateKey = (t: Template): string => JSON.stringify([t.source, t.prefix, t.suffix]); + const copyTemplate = (source: string): Template => ({ source, prefix: [], suffix: [] }); + const mirrors = new Map(); + for (const [id, f] of facts) { + if (variants.has(id) || f.pinned || f.runtime || f.opaque) continue; + const candidate = orderable(id, f); + if (candidate === null || f.literals.length + f.copies.length === 0) continue; + const templates: (Template | null)[] = f.literals.map((segments) => templateOf(segments, id)); + for (const c of f.copies) templates.push(copyTemplate(c.source)); + const first = templates[0]; + if ( + first === null || + first === undefined || + templates.some((t) => t === null || templateKey(t) !== templateKey(first)) + ) + continue; + const orders = variants.get(first.source); + if (orders === undefined || mirrors.has(first.source)) continue; + const table = tableOf(candidate.shape); + const tested = first.suffix.filter((k) => maybeAbsent(first.source, k)); + if (tested.length > 4) continue; + const mirrorOf = (o: readonly string[], lacks: ReadonlySet): string[] => { + const order = [...first.prefix]; + for (const k of o) if (!lacks.has(k) && !order.includes(k)) order.push(k); + for (const k of first.suffix) if (!order.includes(k)) order.push(k); + return complete(order, table); + }; + let mirrored: string[][]; + let template: Template = first; + if (tested.length === 0) mirrored = orders.map((o) => mirrorOf(o, new Set())); + else { + mirrored = []; + const index: number[][] = []; + for (let present = 0; present < 1 << tested.length; present++) { + const lacks = new Set(tested.filter((_, j) => (present & (1 << j)) === 0)); + const row: number[] = []; + for (const o of orders) { + const order = mirrorOf(o, lacks); + let i = indexOfOrder(mirrored, order); + if (i < 0) i = mirrored.push(order) - 1; + row.push(i); + } + index.push(row); + } + if (mirrored.length > MAX_ORDERS) continue; + template = { + source: first.source, + prefix: first.prefix, + suffix: first.suffix, + select: { keys: tested, index }, + }; + } + if (!closeUnderMoves(id, mirrored, candidate.moving)) continue; + if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) { + // Every order of the source agrees over these keys: one table. + moves.delete(id); + const shape = candidate.shape; + const only: string[] = mirrored[0] ?? table; + if (!sameOrder(only, table)) + shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...only]; + continue; + } + variants.set(id, mirrored); + mirrors.set(id, template); + } + if (variants.size === 0) return; + + // Every construction of those shapes, to give each its order index. A + // copy reads its source's index, so its source must be a plain binding. + const sites = new Map(); + /** The statements a spread literal's sequence evaluates first: the + * spread value's binding, then its fields. */ + const prologues = new Map(); + const siteOf = (e: IrExpr): boolean => { + if (e.kind === "seqExpr" && e.result.kind === "recordLit") prologues.set(e.result, e.stmts); + if (e.kind === "recordLit" && e.type.kind === "record" && variants.has(e.type.shapeId)) { + const list = sites.get(e.type.shapeId) ?? []; + list.push(e); + sites.set(e.type.shapeId, list); + } + return everyExprChild(e, siteOf, siteStmt); + }; + const siteStmt = (st: IrStmt): boolean => everyStmtChild(st, siteOf, siteStmt); + for (const fn of fns) for (const st of fn.body) siteStmt(st); + /** The binding a copy or spread reads its fields from. */ + const copySource = (lit: IrExpr & { kind: "recordLit" }, source: string): IrExpr | null => { + for (const field of lit.fields) { + const value = field.value; + if (value.kind === "recordGet" && value.shapeId === source && value.obj.kind === "varRef") + return value.obj; + } + for (const st of prologues.get(lit) ?? []) { + if (st.kind !== "varDecl" || st.init === null) continue; + const t = st.init.type; + if (t.kind === "record" && t.shapeId === source) + return { kind: "varRef", localId: st.localId, type: t, loc: lit.loc }; + } + return null; + }; + const orderIndex = (id: string, lit: IrExpr & { kind: "recordLit" }): IrExpr | number | null => { + const orders = variants.get(id)!; + const segments = notes.literalOf(lit); + if (segments !== undefined && selfSpread(segments, id)) { + const obj = copySource(lit, id); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: id, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + const mirror = mirrors.get(id); + if (segments !== undefined && mirror !== undefined) { + const obj = copySource(lit, mirror.source); + if (obj === null) return null; + const sourceIndex: IrExpr = { + kind: "recordGet", + obj, + shapeId: mirror.source, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + const select = mirror.select; + if (select === undefined) return sourceIndex; + // Tests the source for each key in turn, then picks by its index. + const pick = (j: number, present: number): IrExpr => { + if (j === select.keys.length) + return indexChain(sourceIndex, select.index[present]!, lit.loc); + return { + kind: "ternary", + cond: lowerer.recordFieldPresent(obj, mirror.source, select.keys[j]!, lit.loc), + then: pick(j + 1, present | (1 << j)), + else_: pick(j + 1, present), + type: F64, + loc: lit.loc, + }; + }; + return pick(0, 0); + } + if (segments !== undefined) { + const order = literalOrder(segments); + return order === null ? null : indexOfOrder(orders, complete(order, orders[0]!)); + } + const source = notes.copyOf(lit); + if (source === undefined) return 0; + if (variants.has(source) && (source === id || mirrors.get(id)?.source === source)) { + const obj = copySource(lit, source); + if (obj === null) return null; + return { + kind: "recordGet", + obj, + shapeId: source, + field: KEY_ORDER_FIELD, + type: F64, + loc: lit.loc, + }; + } + const resolved = result.get(source); + if (resolved === undefined || resolved === "unknown") return null; + const keys = new Set( + lit.fields.filter((x) => x.value.kind !== "fieldAbsent").map((x) => x.name), + ); + return indexOfOrder( + orders, + complete( + resolved.order.filter((k) => keys.has(k)), + orders[0]!, + ), + ); + }; + const plan = new Map(); + const dropped = new Set(); + for (const [id, list] of sites) { + for (const lit of list) { + const index = orderIndex(id, lit); + if (index === null || (typeof index === "number" && index < 0)) dropped.add(id); + else plan.set(lit, index); + } + } + // Write sites of keys that move: each reads and updates the value's + // index before the write. + const writeSites: { st: IrStmt & { kind: "recordSet" }; fn: IrFunction }[] = []; + let writeFn: IrFunction | null = null; + const writeExpr = (e: IrExpr): boolean => everyExprChild(e, writeExpr, writeOf); + const writeOf = (st: IrStmt): boolean => { + if ( + st.kind === "recordSet" && + st.value.kind !== "fieldAbsent" && + moves.get(st.shapeId)?.has(st.field) + ) + writeSites.push({ st, fn: writeFn! }); + return everyStmtChild(st, writeExpr, writeOf); + }; + for (const fn of fns) { + writeFn = fn; + for (const st of fn.body) writeOf(st); + } + // A shape whose copies cannot read their source keeps today's table, and + // so do the shapes that mirror it. + for (const [id, mirror] of mirrors) if (dropped.has(mirror.source)) dropped.add(id); + for (const [lit, index] of plan) { + const id = (lit.type as IrType & { kind: "record" }).shapeId; + if (dropped.has(id)) continue; + const value: IrExpr = + typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index; + lit.fields.push({ name: KEY_ORDER_FIELD, value }); + } + let temporaries = 0; + const replacements = new Map>(); + for (const { st, fn } of writeSites) { + if (dropped.has(st.shapeId)) continue; + const transitions = moves.get(st.shapeId)!.get(st.field)!; + const loc = st.loc; + // A receiver other than a binding evaluates once, into a local. So does + // a binding when the value evaluates first (below): the value may + // assign the binding, and the write still goes to the object the + // binding held before. + const pure = + st.value.kind === "numLit" || + st.value.kind === "strLit" || + st.value.kind === "boolLit" || + st.value.kind === "varRef"; + let plain = st.obj; + while (plain.kind === "unionNarrow" || plain.kind === "downcast") plain = plain.value; + const prologue: IrStmt[] = []; + let obj = st.obj; + if (plain.kind !== "varRef" || !pure) { + const localId = `%keyOrderReceiver.${temporaries++}`; + fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false }); + prologue.push({ kind: "varDecl", localId, init: st.obj, loc }); + obj = { kind: "varRef", localId, type: st.obj.type, loc }; + } + const current: IrExpr = { + kind: "recordGet", + obj, + shapeId: st.shapeId, + field: KEY_ORDER_FIELD, + type: F64, + loc, + }; + const next = indexChain(current, transitions, loc); + // The value evaluates before the key is tested and moved: it may add + // other keys to the same value, which then come first. + let value = st.value; + if (!pure) { + const localId = `%keyOrderValue.${temporaries++}`; + fn.locals.push({ id: localId, name: "%keyOrderValue", type: value.type, mutable: false }); + prologue.push({ kind: "varDecl", localId, init: value, loc }); + value = { kind: "varRef", localId, type: value.type, loc }; + } + const absent: IrExpr = { + kind: "unary", + op: "!", + operand: lowerer.recordFieldPresent(obj, st.shapeId, st.field, loc), + type: BOOL, + loc, + }; + const update: IrStmt = { + kind: "recordSet", + obj, + shapeId: st.shapeId, + field: KEY_ORDER_FIELD, + value: next, + loc, + }; + // The write becomes a block that moves the key first. + const write: IrStmt = { + kind: "recordSet", + obj, + shapeId: st.shapeId, + field: st.field, + value, + loc, + }; + const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc }; + const block: IrStmt = { kind: "block", body: [...prologue, guard, write], loc }; + let replaced = replacements.get(fn); + if (replaced === undefined) { + replaced = new Map(); + replacements.set(fn, replaced); + } + replaced.set(st, block); + } + for (const [fn, replaced] of replacements) { + fn.body = transformStmtList(fn.body, { + expr: (e) => e, + stmt: (st) => replaced.get(st) ?? st, + }); + } + for (const [id, orders] of variants) { + if (dropped.has(id)) continue; + const shape = shapes.get(id)!; + const indexKeys: string[] = (shape.declaredOrder ?? []).filter(isArrayIndex); + shapes.addKeyOrderField(id); + const tables: string[][] = []; + for (const order of orders) { + const table: string[] = [...indexKeys]; + for (const key of order) table.push(key); + tables.push(table); + } + shape.orderVariants = tables; + } } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index d18761f98..0074311ec 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -73,6 +74,7 @@ import { isDynTypedRefType, isUnitType, shapeHasAccessorSlots, + shapeKeyOrders, typeEquals, RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; @@ -7140,8 +7142,12 @@ export function recordKeysArrayCall( loc, }, ]; - const order = current.declaredOrder ?? current.fields.map((f) => f.name); - for (const name of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, name] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const f = current.fields.find((x) => x.name === name)!; const pushStmt: IrStmt = { kind: "exprStmt", @@ -7158,12 +7164,13 @@ export function recordKeysArrayCall( // Fields that can be absent push behind their presence test (the // key exists exactly when Object.keys would list it). const present = lowerer.recordFieldPresent(ref, argIr.shapeId, f.name, loc); - body.push( + walks[k]!.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [pushStmt], else_: null, loc } : pushStmt, ); } + body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; @@ -8866,8 +8873,12 @@ function lowerObjectStaticCall( loc, }, ]; - const order = current.declaredOrder ?? current.fields.map((f) => f.name); - for (const name of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, name] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const f = current.fields.find((x) => x.name === name)!; const raw: IrExpr = { kind: "recordGet", @@ -9029,8 +9040,9 @@ function lowerObjectStaticCall( loc, }; } - body.push(stmt); + walks[k]!.push(stmt); } + body.push(...perKeyOrder(current, ref, walks, loc)); body.push({ kind: "return", value: outRef, loc }); fn.body = body; }; diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 7c2ec4a2d..5266d5c4e 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,5 @@ import { keyedHelperName } from "./stable-names.js"; +import { perKeyOrder } from "./key-order-witness.js"; import { dynUndefinedExpr, boolLit, @@ -40,6 +41,7 @@ import { isDynTypedRefType, recordTextCodecClass, shapeHasAccessorSlots, + shapeKeyOrders, typeKey, } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; @@ -1647,7 +1649,12 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { }); } body.push(depthGate("[Object]"), ...begin()); - for (const fname of order) { + // One walk per key order (see perKeyOrder). + const orders = shapeKeyOrders(current); + const walks = orders.map((): IrStmt[] => []); + for (const [k, fname] of orders.flatMap((order, k) => + order.map((name) => [k, name] as const), + )) { const ft = byName.get(fname); if (!ft) continue; const show = entry( @@ -1655,12 +1662,13 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { boolLit(false, loc), ); const present = lowerer.recordFieldPresent(v(), t.shapeId, fname, loc); - body.push( + walks[k]!.push( present.kind === "recordHas" ? { kind: "if", cond: present, then: [show], else_: null, loc } : show, ); } + body.push(...perKeyOrder(current, v(), walks, loc)); body.push( ret(end(strLit("", loc), strLit("{", loc), strLit("}", loc), false, boolLit(false, loc))), ); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 35514d992..8adab0488 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -43,7 +43,7 @@ import { UNDEFINED_T, VOID, } from "../ir/ir.js"; -import { BIGINT_T } from "../ir/ir.js"; +import { BIGINT_T, KEY_ORDER_FIELD } from "../ir/ir.js"; import { literalValues } from "./literal-values.js"; import { remapUnionDiscriminant } from "./union-discriminants.js"; @@ -555,6 +555,21 @@ export class ShapeRegistry { return missing.length === 0 ? order : [...order, ...missing]; } + /** Gives a shape with orderVariants its KEY_ORDER_FIELD, in place, + * after the bodies lowered (key-order-witness.ts adds the field to each + * of the shape's literals). Allocation zeroes it, which every other + * construction keeps. */ + addKeyOrderField(shapeId: string): void { + const shape = this.byId.get(shapeId); + if (!shape || shape.fields.some((f) => f.name === KEY_ORDER_FIELD)) return; + shape.fields = [...shape.fields, { name: KEY_ORDER_FIELD, type: F64 }].sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0, + ); + this.revision++; + const key = this.keyOf(shape.fields, false, shape.indexValue); + if (!this.byKey.has(key)) this.byKey.set(key, shapeId); + } + /** Adds optional fields to an interned shape in place: members a type * assertion attaches to the shape's values (`r as R & { tag: string }` * followed by `view.tag = ...`; see assertion-members.ts). Every value diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 45d046db9..26539b17a 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -172,6 +172,7 @@ const MODULE_FIELDS: Record>, Role> = { "records[].fields[].name": "other", "records[].declaredOrder[]": "other", "records[].accessorOrder[]": "other", + "records[].orderVariants[][]": "other", "unions[].id": "other", "unions[].discriminant.field": "other", "unions[].discriminant.cases[].values[]": "other", diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index c9a316501..7671d35ca 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1697,6 +1697,24 @@ export interface IrRecordShape { * an accessor property named once (see accessorSlotProp). The * checked-dynamic copy lists its keys in this order. */ accessorOrder?: string[]; + /** Every order the shape's values enumerate their keys in, when its + * object literals write the keys in more than one order (see + * key-order-witness.ts); declaredOrder is the first. Each value holds the + * index of its order in the hidden number field KEY_ORDER_FIELD: the + * literal that wrote it stores its order's index, a copy its source's, + * and every other construction leaves 0. Each key-order surface + * dispatches on that field. */ + orderVariants?: string[][]; +} + +/** The hidden field of a shape with orderVariants: the index of the value's + * key order. */ +export const KEY_ORDER_FIELD = "%order"; + +/** The orders a record shape's values enumerate their keys in: each order + * of a shape with orderVariants, or its one table. */ +export function shapeKeyOrders(shape: IrRecordShape): readonly (readonly string[])[] { + return shape.orderVariants ?? [shape.declaredOrder ?? shape.fields.map((f) => f.name)]; } /** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT diff --git a/packages/compiler/test/forin-keys.test.ts b/packages/compiler/test/forin-keys.test.ts index 91d1848fd..b35ff2154 100644 --- a/packages/compiler/test/forin-keys.test.ts +++ b/packages/compiler/test/forin-keys.test.ts @@ -36,7 +36,7 @@ console.log(count(row), count(JSON.parse('{"b":1}'))); expect(llvm).not.toContain("@scr_dyn_for_in_keys("); // Fully required record shapes are marked fixed for the stable walk. expect(llvm).toMatch( - /_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{[^}]*i8 1, ptr null \}/, + /_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{[^}]*i8 1, ptr null, ptr null \}/, ); }); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0fd5249d3..3a1f8fdee 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v34"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v33"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v34"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json new file mode 100644 index 000000000..5048666d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7610-key-order-json.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7610-key-order-json.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..f800c62c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-key-order-keys-values-entries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7611-key-order-keys-values-entries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json new file mode 100644 index 000000000..14745be37 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-key-order-for-in.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7612-key-order-for-in.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json new file mode 100644 index 000000000..50aa11977 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-key-order-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7613-key-order-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json new file mode 100644 index 000000000..888715320 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-key-order-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7614-key-order-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json new file mode 100644 index 000000000..b2a517432 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-key-order-dynamic.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7615-key-order-dynamic.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json new file mode 100644 index 000000000..d4ead81aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7616-key-order-late-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7616-key-order-late-writes.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e17312713..9f856b376 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 60ef9dc49..50da4a78e 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f8dd9a0c0..193352424 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v33(void) {} +void scr_runtime_abi_v34(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index d716f0de3..1b5e28545 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v33(void); +void scr_runtime_abi_v34(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 2828a7935..94bae2c4e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -364,6 +364,12 @@ typedef struct ScrDynTypedRecordOps { /* The field names as an immortal string[], built on first use by * for-in over a record with every field present (atomic slot). */ ScrArr *keys; + /* A shape whose values enumerate their keys in more than one order + * (IrRecordShape.orderVariants) binds a table with only this hook: it + * answers the table of the record's current order, which a write of an + * optional key can change after the record is boxed. NULL in every other + * table. */ + const struct ScrDynTypedRecordOps *(*select)(void *rec); } ScrDynTypedRecordOps; /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b0c22c6ae..0f2e7dff2 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3665,7 +3665,8 @@ bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) { static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) { if (!d->v.typed_ref.record || d->v.typed_ref.materialized) return NULL; - return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; + return ops && ops->select ? ops->select(d->v.typed_ref.ptr) : ops; } const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) { diff --git a/tests/corpus/7610-key-order-json.ts b/tests/corpus/7610-key-order-json.ts new file mode 100644 index 000000000..0cd3dff61 --- /dev/null +++ b/tests/corpus/7610-key-order-json.ts @@ -0,0 +1,31 @@ +// JSON.stringify of object literals that write the same keys in different +// orders: each value serializes in the order its literal wrote, as in Node. +type Point = { x: number; y: number; label?: string }; + +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) console.log(JSON.stringify(value)); +console.log(JSON.stringify([a, b, c])); +console.log(JSON.stringify({ first: b, rest: [c, a] }, null, 2)); + +// Optional keys the literal leaves out, and writes to keys it has. +const p: Point = { y: 2, x: 1 }; +const q: Point = { label: "q", x: 3, y: 4 }; +const r: Point = { x: 5, y: 6, label: "r" }; +q.x = 30; +r.label = "r2"; +for (const point of [p, q, r]) console.log(JSON.stringify(point)); + +// A function typed by one literal's shape receiving the others. +function describe(value: { id: number; name: string; ok: boolean }): string { + return JSON.stringify(value); +} +console.log(describe(c), describe(a)); + +// JSON.parse into the same record type keeps each text's own key order. +type Entry = { id: number; name: string; ok: boolean }; +for (const text of ['{"ok":true,"name":"p","id":7}', '{"id":8,"name":"q","ok":false}']) { + const parsed = JSON.parse(text) as Entry; + console.log(JSON.stringify(parsed), Object.keys(parsed).join(","), describe(parsed)); +} diff --git a/tests/corpus/7611-key-order-keys-values-entries.ts b/tests/corpus/7611-key-order-keys-values-entries.ts new file mode 100644 index 000000000..29f725c24 --- /dev/null +++ b/tests/corpus/7611-key-order-keys-values-entries.ts @@ -0,0 +1,19 @@ +// Object.keys, Object.values, Object.entries and Object.fromEntries over +// object literals that write the same keys in different orders. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + console.log(Object.keys(value).join(",")); + console.log(Object.values(value).join(",")); + console.log(Object.entries(value).map(([key, item]) => `${key}=${String(item)}`).join(" ")); + console.log(JSON.stringify(Object.fromEntries(Object.entries(value)))); +} + +type Row = { year: number; title?: string; score: number }; +const rows: Row[] = [ + { score: 1, year: 2001 }, + { title: "t", year: 2002, score: 2 }, + { year: 2003, score: 3, title: "u" }, +]; +for (const row of rows) console.log(Object.keys(row).join(","), Object.values(row).length); diff --git a/tests/corpus/7612-key-order-for-in.ts b/tests/corpus/7612-key-order-for-in.ts new file mode 100644 index 000000000..89ed16edb --- /dev/null +++ b/tests/corpus/7612-key-order-for-in.ts @@ -0,0 +1,15 @@ +// for-in over object literals that write the same keys in different +// orders, and over values a function received. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +function keysOf(value: { id: number; name: string; ok: boolean }): string { + const keys: string[] = []; + for (const key in value) keys.push(key); + return keys.join(","); +} +for (const value of [a, b, c]) { + const seen: string[] = []; + for (const key in value) seen.push(key.toUpperCase()); + console.log(seen.join(" "), keysOf(value)); +} diff --git a/tests/corpus/7613-key-order-inspect.ts b/tests/corpus/7613-key-order-inspect.ts new file mode 100644 index 000000000..6a11eaf2c --- /dev/null +++ b/tests/corpus/7613-key-order-inspect.ts @@ -0,0 +1,12 @@ +import { inspect } from "node:util"; + +// console.log and util.inspect of object literals that write the same keys +// in different orders, alone and nested. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +console.log(a, b, c); +console.log([a, b, c]); +console.log({ outer: { inner: b }, list: [c] }); +console.log(inspect(b, { depth: 0 }), inspect({ deep: { deeper: c } }, { depth: 0 })); +console.log(`${inspect(c)} / ${inspect(a)}`); diff --git a/tests/corpus/7614-key-order-copies.ts b/tests/corpus/7614-key-order-copies.ts new file mode 100644 index 000000000..5d754bced --- /dev/null +++ b/tests/corpus/7614-key-order-copies.ts @@ -0,0 +1,32 @@ +// Copies of object literals that write the same keys in different orders: +// spreads, spreads with other keys before and after, rest bindings, +// Object.assign and structuredClone keep each value's own order. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +for (const value of [a, b, c]) { + const copy = { ...value }; + const changed = { ...value, id: 10 }; + const after = { ...value, extra: "e" }; + const before = { first: 0, ...value }; + const { name, ...rest } = value; + const assigned = Object.assign({}, value); + const cloned = structuredClone(value); + console.log(JSON.stringify(copy), JSON.stringify(changed)); + console.log(JSON.stringify(after), Object.keys(before).join(",")); + console.log(name, JSON.stringify(rest), Object.keys(rest).join(",")); + console.log(JSON.stringify(assigned), JSON.stringify(cloned)); +} +// Keys after a spread that the spread value may lack: a key the value has +// keeps its place, and a key it lacks goes after the value's keys. +type Options = { id: number; label?: string; size?: number; tag?: string }; +const options: Options[] = [ + { id: 1, size: 2 }, + { size: 3, id: 2 }, + { label: "c", id: 3, tag: "t" }, + { tag: "u", size: 4, id: 4 }, +]; +for (const value of options) { + const merged = { ...value, tag: "x", label: "y", done: true }; + console.log(Object.keys(merged).join(","), JSON.stringify(merged)); +} diff --git a/tests/corpus/7615-key-order-dynamic.ts b/tests/corpus/7615-key-order-dynamic.ts new file mode 100644 index 000000000..55970cb40 --- /dev/null +++ b/tests/corpus/7615-key-order-dynamic.ts @@ -0,0 +1,30 @@ +import { MessageChannel } from "node:worker_threads"; + +// Object literals that write the same keys in different orders, seen +// through untyped values: their keys, JSON text, own property names, and a +// MessageChannel message. +const a = { id: 1, name: "a", ok: true }; +const b = { name: "b", ok: false, id: 2 }; +const c = { ok: true, id: 3, name: "c" }; +const values: unknown[] = [a, b, c]; +for (const value of values) { + const object = value as Record; + console.log(Object.keys(object).join(","), JSON.stringify(object)); + console.log(Object.getOwnPropertyNames(object).join(",")); +} +const { port1, port2 } = new MessageChannel(); +let received = 0; +port1.on("message", (message: unknown) => { + console.log("message", JSON.stringify(message), Object.keys(message as object).join(",")); + if (++received === 3) port1.close(); +}); +for (const value of [a, b, c]) port2.postMessage(value); + +// A nested value seen through an untyped view keeps its identity and its +// own key order, and writes through the view reach it. +const holder = { inner: b, tag: "h" }; +const view = holder as unknown as Record; +const inner = view.inner as Record; +console.log(view.inner === view.inner, Object.keys(inner).join(",")); +inner.id = 20; +console.log(b.id, JSON.stringify(b), JSON.stringify(view)); diff --git a/tests/corpus/7616-key-order-late-writes.ts b/tests/corpus/7616-key-order-late-writes.ts new file mode 100644 index 000000000..4166999d7 --- /dev/null +++ b/tests/corpus/7616-key-order-late-writes.ts @@ -0,0 +1,44 @@ +// Optional keys added after construction go last, as in Node, also for +// shapes whose literals write their keys in different orders: a write of a +// missing key, a delete and a later write, and writes in a loop. +type Item = { id: number; name?: string; note?: string; count: number }; + +const items: Item[] = [ + { id: 1, count: 0 }, + { count: 1, name: "b", id: 2 }, + { note: "c", id: 3, count: 2 }, +]; +items[0]!.note = "added"; +items[0]!.name = "later"; +items[1]!.name = "kept"; +delete items[2]!.note; +items[2]!.note = "again"; +for (const item of items) { + if (item.count > 0) item.name ??= "filled"; + console.log(JSON.stringify(item), Object.keys(item).join(",")); +} +const built: Item = { count: 9, id: 9 }; +for (let i = 0; i < 2; i++) { + if (i === 1) built.note = `n${i}`; + console.log(Object.keys(built).join(",")); +} +console.log(built); +// A write's value runs first, so a key it adds comes before the written +// key. +const nested: Item = { count: 3, id: 4 }; +nested.name = ((nested.note = "first"), "second"); +console.log(Object.keys(nested).join(","), JSON.stringify(nested)); +// A value seen as a JavaScript value enumerates later writes in order. +const seen: Item = { id: 5, count: 4 }; +const view: unknown = seen; +console.log(Object.keys(view as object).join(",")); +seen.note = "n"; +seen.name = "m"; +console.log(Object.keys(view as object).join(","), JSON.stringify(view)); +// The write goes to the object the receiver held before the value ran, +// even when the value assigns the receiver's binding. +let target: Item = { id: 6, count: 5 }; +const before = target; +const after: Item = { count: 6, id: 7 }; +target.name = ((target = after), "moved"); +console.log(Object.keys(before).join(","), Object.keys(after).join(","), JSON.stringify(target)); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index 83c3ede49..0cf8bcae5 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,18 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: values of one type are built in two orders and the program -// observes them, so the type keeps its declaration order. +// Key order: values of one type are built in two orders, and a spread sets +// an optional key its source lacks, so the values cannot record their own +// orders and the type keeps its declaration order. interface Point { x: number; y: number; + label?: string; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); +const other: Point = { x: 3, y: 4 }; +const labeled: Point = { ...point, label: "p" }; +console.log(JSON.stringify(point), JSON.stringify(other), JSON.stringify(labeled)); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/harness/__snapshots__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt index 66373655f..8ab286873 100644 --- a/tests/harness/__snapshots__/coverage-divergences.txt +++ b/tests/harness/__snapshots__/coverage-divergences.txt @@ -1,7 +1,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts - statements analyzed 18 - compile statically 18 (100%) + statements analyzed 20 + compile statically 20 (100%) in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) fully static — all analyzed statements compile to native code. @@ -14,11 +14,11 @@ scriptc coverage tests/coverage-fixtures/divergences.ts at tests/coverage-fixtures/divergences.ts:20:23 hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class ×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in … - at tests/coverage-fixtures/divergences.ts:29:22 + at tests/coverage-fixtures/divergences.ts:31:22 hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built) ×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation - at tests/coverage-fixtures/divergences.ts:33:70 + at tests/coverage-fixtures/divergences.ts:37:70 hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree ×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally - at tests/coverage-fixtures/divergences.ts:48:20 + at tests/coverage-fixtures/divergences.ts:52:20 hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share \ No newline at end of file diff --git a/tests/harness/corpus-costs/7610-key-order-json.ts.json b/tests/harness/corpus-costs/7610-key-order-json.ts.json new file mode 100644 index 000000000..aa8658007 --- /dev/null +++ b/tests/harness/corpus-costs/7610-key-order-json.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json b/tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json new file mode 100644 index 000000000..58d7b29f8 --- /dev/null +++ b/tests/harness/corpus-costs/7611-key-order-keys-values-entries.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 2800 } diff --git a/tests/harness/corpus-costs/7612-key-order-for-in.ts.json b/tests/harness/corpus-costs/7612-key-order-for-in.ts.json new file mode 100644 index 000000000..e0b1e4dd1 --- /dev/null +++ b/tests/harness/corpus-costs/7612-key-order-for-in.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2100 } diff --git a/tests/harness/corpus-costs/7613-key-order-inspect.ts.json b/tests/harness/corpus-costs/7613-key-order-inspect.ts.json new file mode 100644 index 000000000..932ea4809 --- /dev/null +++ b/tests/harness/corpus-costs/7613-key-order-inspect.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2500 } diff --git a/tests/harness/corpus-costs/7614-key-order-copies.ts.json b/tests/harness/corpus-costs/7614-key-order-copies.ts.json new file mode 100644 index 000000000..9d76a548c --- /dev/null +++ b/tests/harness/corpus-costs/7614-key-order-copies.ts.json @@ -0,0 +1 @@ +{ "plain": 1900, "san": 3500 } diff --git a/tests/harness/corpus-costs/7615-key-order-dynamic.ts.json b/tests/harness/corpus-costs/7615-key-order-dynamic.ts.json new file mode 100644 index 000000000..12680c6af --- /dev/null +++ b/tests/harness/corpus-costs/7615-key-order-dynamic.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2700 } diff --git a/tests/harness/corpus-costs/7616-key-order-late-writes.ts.json b/tests/harness/corpus-costs/7616-key-order-late-writes.ts.json new file mode 100644 index 000000000..2b1aad28d --- /dev/null +++ b/tests/harness/corpus-costs/7616-key-order-late-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2800 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 4d5546ab6..669a3a0d6 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From da289bb593ef27c17e45c0121b63c61fdee1dbdb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:13:49 -0500 Subject: [PATCH 120/169] Support writev on Transform and PassThrough, and runtime stream option values --- .../builtin-apis/streams-and-readline.mdx | 4 +- packages/cli/test/native-link-info.test.ts | 4 +- .../src/backend/llvm/expr-stream-callbacks.ts | 11 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-classes.ts | 6 +- .../src/frontend/lowering/lower-stream.ts | 230 ++++++++++++++++-- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 5 + packages/compiler/src/ir/runtime-features.ts | 2 + packages/compiler/src/ir/validate.ts | 2 + .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7460-transform-writev.mjs.json | 6 + .../corpus/7461-transform-writev.cjs.json | 6 + .../corpus/7462-transform-writev.ts.json | 6 + .../corpus/7463-stream-option-values.ts.json | 6 + .../corpus/7464-stream-option-values.mjs.json | 6 + .../7465-transform-callback-values.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn_builtin_stream.c | 9 +- packages/runtime/src/scr_stream.c | 77 +++++- packages/runtime/src/scr_stream.h | 8 + tests/corpus/7460-transform-writev.mjs | 33 +++ tests/corpus/7461-transform-writev.cjs | 33 +++ tests/corpus/7462-transform-writev.ts | 109 +++++++++ tests/corpus/7463-stream-option-values.ts | 58 +++++ tests/corpus/7464-stream-option-values.mjs | 28 +++ .../corpus/7465-transform-callback-values.ts | 41 ++++ .../7460-transform-writev.mjs.json | 1 + .../7461-transform-writev.cjs.json | 1 + .../7462-transform-writev.ts.json | 1 + .../7463-stream-option-values.ts.json | 1 + .../7464-stream-option-values.mjs.json | 1 + .../7465-transform-callback-values.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 40 files changed, 670 insertions(+), 57 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json create mode 100644 tests/corpus/7460-transform-writev.mjs create mode 100644 tests/corpus/7461-transform-writev.cjs create mode 100644 tests/corpus/7462-transform-writev.ts create mode 100644 tests/corpus/7463-stream-option-values.ts create mode 100644 tests/corpus/7464-stream-option-values.mjs create mode 100644 tests/corpus/7465-transform-callback-values.ts create mode 100644 tests/harness/corpus-costs/7460-transform-writev.mjs.json create mode 100644 tests/harness/corpus-costs/7461-transform-writev.cjs.json create mode 100644 tests/harness/corpus-costs/7462-transform-writev.ts.json create mode 100644 tests/harness/corpus-costs/7463-stream-option-values.ts.json create mode 100644 tests/harness/corpus-costs/7464-stream-option-values.mjs.json create mode 100644 tests/harness/corpus-costs/7465-transform-callback-values.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index fe6bb2dfb..b88ead1f4 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -32,9 +32,9 @@ `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. - The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported. + The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side. + In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write). `setDefaultEncoding()` takes any encoding Node knows. - A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. `on`, `once`, and `addListener` return the interface, so registrations chain. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index bf5b3be14..8715d9356 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v33"); + expect(error?.stderr).toContain("scr_runtime_abi_v34"); }); }); diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 14a2e3432..b368c91f4 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -727,6 +727,17 @@ export function streamCbThunkFor( const ty = host.llType(p); host.declare(`declare zeroext i1 @scr_exc_pending()`); if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + // An entry `encoding` declared as a string (the declarations' + // `encoding: string`) receives "" where Node passes undefined or + // null (an object-mode write), as a write's encoding parameter does. + const entry = + p.kind === "array" && p.elem.kind === "record" + ? host.recordsById.get(p.elem.shapeId) + : undefined; + if (entry?.fields.some((f) => f.name === "encoding" && f.type.kind === "string")) { + host.declare(`declare void @scr_stream_writev_fill_encodings(ptr)`); + d.push(` call void @scr_stream_writev_fill_encodings(ptr %chunks)`); + } d.push( ` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`, ` %vtbad${i} = call zeroext i1 @scr_exc_pending()`, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 054d9341b..548e8e597 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1135,6 +1135,8 @@ export const LIB_FN_SYMS: Record = { "readable.pushEncoding": "scr_stream_set_push_encoding", "stream.objectMode": "scr_stream_object_mode", "stream.decodeStrings": "scr_stream_decode_strings", + "stream.optionValues": "scr_stream_option_values", + "stream.checkEncodingOption": "scr_stream_check_encoding_option", "stream.duplexFromGen": "scr_stream_duplex_from_gen", "stream.duplexFromDyn": "scr_stream_duplex_from_dyn", "stream.duplexifyReadable": "scr_stream_duplexify_readable", diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 7d3dcbc02..173de6408 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 33 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const; +export const RUNTIME_ABI_VERSION = 34 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v34" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 532051b88..923b55c5b 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 18b04b9a6..681f86817 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6dd0124bd..e3ab29dba 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2734,8 +2734,8 @@ export function collectClassShapeInner( // state, so an override or shadowing field would split-brain. // Underscore methods are the SUPPORTED override form — but only // the ones the class's own base consumes (a `_read` on a - // Transform, or `_writev`/`_construct` anywhere, would be consumed - // by Node machinery that has no lowering here). + // Transform, or `_writev` on a Readable, would be consumed by Node + // machinery that has no lowering here). if (memberName && ts.isIdentifier(memberName)) { const streamBase = (() => { for (let c = base; c; c = c.base) if (c.builtinStream) return c; @@ -2754,7 +2754,7 @@ export function collectClassShapeInner( lowerer.unsupported( "SC1090", memberName, - `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`, + `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (a Readable has no writable side)`, ); } const accepted = streamCtorShape(streamBase.def.name).accepted; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index c37ae9074..15afb741e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -179,15 +179,15 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ /** The option callbacks applied after construction rather than through * the constructor's callback slots (option name → method name): construct - * on every stream class, and writev on the writable classes whose - * machinery hands it batches (Writable and Duplex). */ + * on every stream class, and writev on every class with a writable side + * (a Transform's batches reach writev, not the transform, as in Node). */ export const STREAM_EXTRA_CALLBACKS: ReadonlyMap = new Map([ ["construct", "_construct"], ["writev", "_writev"], ]); export function streamExtraCallbacks(cls: string): readonly string[] { - return cls === "%Writable" || cls === "%Duplex" ? ["construct", "writev"] : ["construct"]; + return cls === "%Readable" ? ["construct"] : ["construct", "writev"]; } /** The completion callback's parameter position in each underscore @@ -442,6 +442,13 @@ interface StreamOptions { * right after construction (Node's constructors call addAbortSignal * once the state exists), or null when absent. */ signal: IrExpr | null; + /** Non-literal decodeStrings and defaultEncoding values: valueDecls + * evaluates them into locals before construction (the defaultEncoding + * validated there, as the state constructors throw before the stream + * exists), and stream.optionValues applies them to the fresh stream. */ + valueDecls: IrStmt[]; + decodeStringsValue: IrExpr | null; + defaultEncodingValue: IrExpr | null; /** The objectMode option's sides (a Duplex's readableObjectMode and * writableObjectMode set one side): applied with stream.objectMode * right after construction. */ @@ -732,6 +739,12 @@ function pinAnyDataCompletion( if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null; const declared = lowerer.paramShape(param).type; if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null; + // Only when every use calls the callback with a Buffer, a string or + // nothing as `data`: another value (or a callback that escapes) keeps + // `data` checked-dynamic, so the runtime pushes it as push(data) does + // and reports ERR_INVALID_ARG_TYPE through 'error', as Node does. + if (fnNode.body === undefined || !completionDataIsChunk(lowerer, fnNode.body, symbol, param.name)) + return null; const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]); lowerer.stageSourceTypes.set( symbol, @@ -740,6 +753,59 @@ function pinAnyDataCompletion( return symbol; } +/** Whether every reference to the completion callback `symbol` in `body` + * calls it with a `data` argument that is a Buffer, a string, null or + * undefined (or with no data). */ +function completionDataIsChunk( + lowerer: Lowerer, + body: ts.Node, + symbol: ts.Symbol, + declaration: ts.Node, +): boolean { + let ok = true; + const visit = (node: ts.Node): void => { + if (!ok) return; + if ( + ts.isIdentifier(node) && + node !== declaration && + lowerer.checker.getSymbolAtLocation(node) === symbol + ) { + const call = node.parent; + if (!call || !ts.isCallExpression(call) || call.expression !== node) { + ok = false; + return; + } + const data = call.arguments.nodes[1]; + if (data !== undefined) { + let t: IrType | null; + try { + t = lowerer.mapTypeOf(lowerer.typeOf(data)); + } catch { + t = null; + } + const arms = + t?.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? null) : t ? [t] : null; + if ( + arms === null || + !arms.every( + (a) => + (a.kind === "bytes" && a.elem === "u8") || + a.kind === "string" || + a.kind === "undefinedT" || + a.kind === "nullT", + ) + ) { + ok = false; + return; + } + } + } + ts.forEachChild(node, visit); + }; + visit(body); + return ok; +} + /** Parses the options object literal of a stream constructor. `which` * names the callbacks this class accepts, in canonical flag order. */ function parseStreamOptions( @@ -767,6 +833,9 @@ function parseStreamOptions( badEncoding: null, pushEncoding: null, signal: null, + valueDecls: [], + decodeStringsValue: null, + defaultEncodingValue: null, readableObject: false, writableObject: false, decodeStringsOff: false, @@ -802,6 +871,21 @@ function parseStreamOptions( optsLit !== undefined && ts.isObjectLiteralExpression(optsLit) && literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable"); + // In JavaScript a decodeStrings value known only at run time may let + // string writes through, so the write and transform callbacks' chunk + // parameters read as any (the declarations type it Buffer). + const runtimeDecodeStrings = + optsLit !== undefined && + isJsSourceFile(ts.getSourceFileOfNode(optsLit)) && + ts.isObjectLiteralExpression(optsLit) && + (optsLit.properties?.nodes ?? []).some( + (p) => + ts.isPropertyAssignment(p) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && + p.name.text === "decodeStrings" && + p.initializer.kind !== ts.SyntaxKind.TrueKeyword && + p.initializer.kind !== ts.SyntaxKind.FalseKeyword, + ); if (out.readableObject) out.hwmR = numLit(16, loc); if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); @@ -851,6 +935,7 @@ function parseStreamOptions( } const full = tuples[name]!.map((t) => (t === "fn" ? null : t)); const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode); + let anyChunk: ts.Symbol | undefined; try { // The completion-callback tail: typed by the checker contextually; // accept the user's declared func type as the tuple's entry so the @@ -867,12 +952,22 @@ function parseStreamOptions( } return declared ?? funcOf([], VOID); }); + const chunkParam = + runtimeDecodeStrings && (name === "write" || name === "transform") + ? fnNode.parameters.nodes[0] + : undefined; + anyChunk = + chunkParam !== undefined && ts.isIdentifier(chunkParam.name) && !chunkParam.type + ? lowerer.checker.getSymbolAtLocation(chunkParam.name) + : undefined; + if (anyChunk) lowerer.anySymbols.add(anyChunk); present.set( name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues), ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); + if (anyChunk) lowerer.anySymbols.delete(anyChunk); } continue; } @@ -925,11 +1020,15 @@ function parseStreamOptions( if (writableClass) out.decodeStringsOff = true; break; } - lowerer.noLowering( - `${ctorName} with a runtime 'decodeStrings' value`, - prop, - "write 'decodeStrings: true' or 'decodeStrings: false'", - ); + // a runtime value: WritableState's `options.decodeStrings !== false` + const local = lowerer.declareHiddenLocal("%decodeStrings", DYN); + out.valueDecls.push({ + kind: "varDecl", + localId: local.id, + init: lowerer.lowerExprExpecting(value, DYN), + loc, + }); + if (writableClass) out.decodeStringsValue = varRef(local.id, DYN, loc); break; } case "encoding": { @@ -973,11 +1072,33 @@ function parseStreamOptions( break; } } - lowerer.noLowering( - `the ${ctorName} option 'defaultEncoding' with a runtime value`, - prop, - "write the encoding as a string literal", - ); + { + // a runtime value: null, undefined and utf8 are the default, + // another name Buffer.isEncoding knows is kept as given, and any + // other value throws ERR_UNKNOWN_ENCODING before construction + const local = lowerer.declareHiddenLocal("%defaultEncoding", DYN); + const ref = varRef(local.id, DYN, loc); + out.valueDecls.push( + { + kind: "varDecl", + localId: local.id, + init: lowerer.lowerExprExpecting(value, DYN), + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.checkEncodingOption", + args: [ref], + type: VOID, + loc, + }, + loc, + }, + ); + out.defaultEncodingValue = ref; + } break; case "signal": { // `if (options.signal) addAbortSignal(options.signal, this)`: @@ -1041,9 +1162,7 @@ function parseStreamOptions( lowerer.noLowering( `the ${ctorName} option 'writev'`, prop, - ctorName === "Readable" - ? "a Readable has no writable side" - : "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex", + "a Readable has no writable side", ); break; default: @@ -1083,6 +1202,9 @@ function streamCtorArgs( badEncoding: string | null; pushEncoding: string | null; signal: IrExpr | null; + valueDecls: IrStmt[]; + decodeStringsValue: IrExpr | null; + defaultEncodingValue: IrExpr | null; readableObject: boolean; writableObject: boolean; decodeStringsOff: boolean; @@ -1117,6 +1239,9 @@ function streamCtorArgs( badEncoding: o.badEncoding, pushEncoding: o.pushEncoding, signal: o.signal, + valueDecls: o.valueDecls, + decodeStringsValue: o.decodeStringsValue, + defaultEncodingValue: o.defaultEncodingValue, readableObject: o.readableObject, writableObject: o.writableObject, decodeStringsOff: o.decodeStringsOff, @@ -1237,6 +1362,18 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } + const values = optionValuesArgs(parsed, loc); + if (values) { + // runtime decodeStrings and defaultEncoding values (answers the + // receiver +1) + built = { + kind: "libCall", + fn: "stream.optionValues", + args: [built, ...values], + type: thisType, + loc, + }; + } if (parsed.pushEncoding !== null) { // { defaultEncoding }: chain the push-side decode default (the // setEncoding chaining shape — answers the receiver +1). @@ -1269,8 +1406,11 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) - return built; + if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) { + return parsed.valueDecls.length > 0 + ? { kind: "seqExpr", stmts: parsed.valueDecls, result: built, type: thisType, loc } + : built; + } const fresh = lowerer.declareHiddenLocal("%stream", thisType); const followUps = streamOptionFollowUps( parsed, @@ -1280,13 +1420,30 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C ); return { kind: "seqExpr", - stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps], + stmts: [ + ...parsed.valueDecls, + { kind: "varDecl", localId: fresh.id, init: built, loc }, + ...followUps, + ], result: varRef(fresh.id, thisType, loc), type: thisType, loc, }; } +/** stream.optionValues' (decodeStrings, defaultEncoding) arguments, or + * null when both options are literals or absent. */ +function optionValuesArgs( + parsed: { decodeStringsValue: IrExpr | null; defaultEncodingValue: IrExpr | null }, + loc: SrcLoc, +): IrExpr[] | null { + if (parsed.decodeStringsValue === null && parsed.defaultEncodingValue === null) return null; + return [ + parsed.decodeStringsValue ?? dynUndefinedExpr(loc), + parsed.defaultEncodingValue ?? dynUndefinedExpr(loc), + ]; +} + /** True when a value of Node's argument type `passed` widens implicitly * into the declared parameter type `declared` (one of its union arms, or a * sub-union of it). */ @@ -1589,6 +1746,7 @@ export function lowerStreamSuperCall( ]; } const out: IrStmt[] = [ + ...parsed.valueDecls, { kind: "exprStmt", expr: { @@ -1614,6 +1772,20 @@ export function lowerStreamSuperCall( loc, }); } + const values = optionValuesArgs(parsed, loc); + if (values) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.optionValues", + args: [thisRef(), ...values], + type: thisType, + loc, + }, + loc, + }); + } if (parsed.pushEncoding !== null) { out.push({ kind: "exprStmt", @@ -1699,12 +1871,20 @@ export function lowerStreamUnderscoreAssign( const clsName = cls.slice(1); if (methodName === "_construct") { // Node's constructor reads _construct once; a later assignment has no - // effect there - lowerer.noLowering( - `assigning '${methodName}' on a ${clsName}`, - expr.left, - "declare _construct on the class or pass the construct option", - ); + // effect on the stream, so only the receiver and the value are + // evaluated + const value = lowerer.lowerExpr(expr.right); + return { + kind: "exprStmt", + expr: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: value, + type: value.type, + loc, + }, + loc, + }; } const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d8e78be08..e786fa9a8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -362,6 +362,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "transform.newDyn", "passthrough.newDyn", "readable.initDyn", + "stream.checkEncodingOption", "writable.initDyn", "duplex.initDyn", "transform.initDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7cb666ccf..ed135a800 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4103,6 +4103,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * after construction; answers the receiver (+1, the chaining shape). */ "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + /** Runtime decodeStrings and defaultEncoding option values, applied + * after construction (answers the receiver +1); the encoding was + * validated by stream.checkEncodingOption before construction. */ + "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, + "stream.checkEncodingOption": { argTypes: [DYN], result: VOID }, "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, /** Duplex.from over a lone Readable or Writable, or a { readable, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85910369a..bd4727b95 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -219,6 +219,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.chunkDone", "stream.objectMode", "stream.decodeStrings", + "stream.optionValues", + "stream.checkEncodingOption", "stream.duplexFromGen", "stream.duplexFromDyn", "stream.duplexFromPromise", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 770fee6c6..1f3692c9c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5495,6 +5495,7 @@ function validateFunction( e.fn === "stream.destroyErr" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.optionValues" || e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn" || e.fn === "stream.duplexifyReadable" || @@ -5867,6 +5868,7 @@ function validateFunction( e.fn === "readable.pushEncoding" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.optionValues" || e.fn === "writable.writeEncoding" || e.fn === "writable.setDefaultEncoding" || e.fn === "stream.destroy" || diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0fd5249d3..3a1f8fdee 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v34"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v33"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v34"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json new file mode 100644 index 000000000..1015e1354 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7460-transform-writev.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json new file mode 100644 index 000000000..4d3c86215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7461-transform-writev.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json new file mode 100644 index 000000000..9b42f2d60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7462-transform-writev.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json new file mode 100644 index 000000000..5aa543dea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7463-stream-option-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json new file mode 100644 index 000000000..2a438d418 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7464-stream-option-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json new file mode 100644 index 000000000..17633d208 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7465-transform-callback-values.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e17312713..9f856b376 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 60ef9dc49..50da4a78e 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f8dd9a0c0..193352424 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v33(void) {} +void scr_runtime_abi_v34(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index d716f0de3..1b5e28545 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v33(void); +void scr_runtime_abi_v34(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index 926f85eea..2875cfa06 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -34,7 +34,7 @@ * receiver without a reference; the hook closures outlive the record. * * Not supported here (each throws the "not supported yet" Error where it - * runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() / + * runs): the options scr_stream.c's dyn option walk refuses, symbol event names, listeners() / * rawListeners(), `wrap()`, and pipes into values that are neither runtime * streams nor constructor-function streams. */ #include "scr_runtime.h" @@ -898,20 +898,15 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } bool failed = false; bool writes = kind != BCS_READABLE; - bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; ScrDyn *opts = bcs_arg(args, argc, 0); /* Node's constructors assign options.writev and options.construct to * the instance (this._writev = options.writev), then read _writev and * _construct, so an option shadows the prototype's method. */ if (opts->kind == SCR_DYN_OBJ) { - if (writes && !transformish && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL; + if (writes && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL; if (!bcs_copy_option(self, opts, "construct", "_construct")) return NULL; } bool has_writev = writes && bcs_is_function(self, "_writev", &failed); - if (has_writev && transformish) { - bcs_unsupported("_writev on a constructor-function Transform is not supported yet"); - return NULL; - } bool has_construct = !failed && bcs_is_function(self, "_construct", &failed); if (failed) return NULL; ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 275c03039..4b962675a 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2157,6 +2157,24 @@ static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc) return scr_dyn_retain(enc ? enc : scr_dyn_undefined()); } +/* A compiled writev whose entries declare `encoding` a string: an entry + * without one (an object-mode write passes undefined or null) gets "". + * The batch array is the one this call receives. */ +void scr_stream_writev_fill_encodings(ScrDyn *chunks) { + if (!chunks || chunks->kind != SCR_DYN_ARR) return; + double n = scr_dyn_arr_len(chunks); + for (double i = 0; i < n; i++) { + ScrDyn *entry = scr_dyn_arr_at(chunks, i); /* +1 */ + ScrDyn *enc = entry->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(entry, "encoding", 8) : NULL; + if (entry->kind == SCR_DYN_OBJ && (!enc || enc->kind == SCR_DYN_UNDEF || enc->kind == SCR_DYN_NULL)) { + ScrStr *empty = scr_str_new("", 0); + scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_new_str(empty)); + scr_str_release(empty); + } + scr_dyn_release(entry); + } +} + /* A batch entry's callback: the write's callback, or Node's nop. */ static ScrDyn *scr_stream_writev_entry_cb_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { (void)args; @@ -2675,9 +2693,8 @@ void *scr_stream_native(ScrStream *s, void (*release)(void *)) { * way the compiler-emitted dyn thunks do (chunk as Buffer-flavored bytes, * encoding 'buffer', the error via the boundary encoding, the completion * callback as a callable dyn over the *_done glue). Options that Node's - * base WOULD consume but have no lowering here (objectMode truthy, - * decodeStrings:false, non-utf8 defaultEncoding, signal, construct, - * writev, and read/write/final on the transformish bases) throw the + * base WOULD consume but have no lowering here (signal, and + * read/write/final on the transformish bases) throw the * "not supported yet" Error — the compile-time fence's runtime twin. */ /* The boxed dyn callable (+1 — scr_box_get_ref retains; callers release @@ -2903,11 +2920,8 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy } } out->construct = scr_stream_dynopt_fn(opts, "construct", 9); + /* a Transform's batches reach writev, not the transform, as in Node */ if (base != 'r') out->writev = scr_stream_dynopt_fn(opts, "writev", 6); - if (transformish && out->writev != NULL) { - static const char m[] = "the stream option 'writev' on a Transform is not supported yet (batches would bypass the transform)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } { /* a known encoding or ERR_UNKNOWN_ENCODING (the states' constructors) */ ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "defaultEncoding", 15); @@ -6296,6 +6310,55 @@ ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) { return scr_stream_retain(s); } +/* Whether a defaultEncoding option value is the default: null, undefined, + * 'utf8' or 'utf-8' (the state constructors' kDefaultUTF8Encoding). */ +static bool scr_stream_default_encoding_value(const ScrDyn *v) { + if (!v || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) return true; + return v->kind == SCR_DYN_STR && ((v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) || + (v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0)); +} + +/* A runtime defaultEncoding option value, checked before construction as + * the state constructors check it: the default, a name Buffer.isEncoding + * knows, or ERR_UNKNOWN_ENCODING (thrown). Borrows v. */ +void scr_stream_check_encoding_option(ScrDyn *v) { + if (scr_stream_default_encoding_value(v)) return; + if (v->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str); + if (canonical) scr_str_release(canonical); + return; + } + ScrStr *shown = scr_dyn_string_coerce_js(v); + if (!shown) return; + char msg[128]; + int len = snprintf(msg, sizeof msg, "Unknown encoding: %.*s", (int)(shown->len < 64 ? shown->len : 64), + shown->data); + scr_str_release(shown); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)(len < 0 ? 0 : len), "ERR_UNKNOWN_ENCODING"); +} + +/* Runtime decodeStrings and defaultEncoding option values over a fresh + * stream (after its objectMode): decodeStrings === false keeps string + * writes, and a known defaultEncoding is the write side's default as + * given and how a byte-mode readable side decodes push(string). Borrows + * both; answers the receiver (+1). */ +ScrStream *scr_stream_option_values(ScrStream *s, ScrDyn *decode_strings, ScrDyn *default_encoding) { + ScrStreamState *st = s->st; + if (st->has_w && decode_strings && decode_strings->kind == SCR_DYN_BOOL && !decode_strings->v.b) + scr_stream_set_decode_strings(s, false); + if (!scr_stream_default_encoding_value(default_encoding) && default_encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(default_encoding->v.str); + if (canonical) { + scr_stream_set_write_encoding(s, default_encoding->v.str); + if (st->has_r && !st->r.object_entries) scr_stream_release(scr_stream_set_push_encoding(s, canonical)); + scr_str_release(canonical); + } else if (scr_exc_pending()) { + scr_exc_clear(); /* checked before construction */ + } + } + return scr_stream_retain(s); +} + /* stream.objectMode: scr_stream_set_object_mode, answering the receiver * (+1). */ ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index d26077b5c..e368ec7ff 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -563,6 +563,9 @@ void scr_stream_set_writev_concat(ScrStream *s); void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +/* A compiled writev whose entries declare `encoding` a string: entries + * without an encoding (object mode) get "" before the conversion. */ +void scr_stream_writev_fill_encodings(struct ScrDyn *chunks); /* Duplex.from (duplexify.js): over an async generator function (the * compiled stage takes the source generator that the compiled source * makes from next() and ack(); all borrowed, helpers too), and over a @@ -605,6 +608,11 @@ ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box) * forms answer the stream (+1). */ void scr_stream_set_decode_strings(ScrStream *s, bool decode); ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode); +/* Runtime decodeStrings/defaultEncoding option values: checked before + * construction (ERR_UNKNOWN_ENCODING thrown), then applied to the fresh + * stream, answering it (+1). Values borrowed. */ +void scr_stream_check_encoding_option(struct ScrDyn *v); +ScrStream *scr_stream_option_values(ScrStream *s, struct ScrDyn *decode_strings, struct ScrDyn *default_encoding); void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc); ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc); ScrStream *scr_stream_set_default_encoding(ScrStream *s, struct ScrDyn *enc); diff --git a/tests/corpus/7460-transform-writev.mjs b/tests/corpus/7460-transform-writev.mjs new file mode 100644 index 000000000..d78bdfc77 --- /dev/null +++ b/tests/corpus/7460-transform-writev.mjs @@ -0,0 +1,33 @@ +// writev on a Transform and a PassThrough (the writev option): a batch of +// buffered writes reaches writev instead of the transform, a lone write +// still reaches the transform, and the readable side gets what writev +// pushes. +import { Transform, PassThrough } from "node:stream"; +const t = new Transform({ + transform(chunk, enc, cb) { console.log("transform", String(chunk), enc); cb(null, chunk); }, + writev(chunks, cb) { + console.log("writev", chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")); + for (const { chunk } of chunks) this.push(String(chunk).toUpperCase()); + cb(); + }, +}); +t.on("data", (d) => console.log("data", String(d))); +t.on("finish", () => console.log("finish")); +t.on("end", () => console.log("end")); +t.write("a"); +t.cork(); +t.write("b"); +t.write("c", "latin1"); +process.nextTick(() => { t.uncork(); t.end("d"); }); + +const p = new PassThrough({ + objectMode: true, + writev(chunks, cb) { console.log("pt writev", JSON.stringify(chunks.map((c) => c.chunk))); cb(); }, +}); +p.on("data", (d) => console.log("pt data", JSON.stringify(d))); +p.cork(); +p.write({ x: 1 }); +p.write([2]); +p.uncork(); +p.write("solo"); +p.end(); diff --git a/tests/corpus/7461-transform-writev.cjs b/tests/corpus/7461-transform-writev.cjs new file mode 100644 index 000000000..dd45cf09d --- /dev/null +++ b/tests/corpus/7461-transform-writev.cjs @@ -0,0 +1,33 @@ +// Constructor-function Transform and PassThrough streams with _writev: +// the prototype's _writev takes the batches, and a writev option shadows +// it with the stream as this. +'use strict'; +const { Transform, PassThrough } = require('node:stream'); +function T(opts) { Transform.call(this, opts); } +Object.setPrototypeOf(T.prototype, Transform.prototype); +Object.setPrototypeOf(T, Transform); +T.prototype._transform = function (chunk, enc, cb) { cb(null, 'T(' + chunk + ')'); }; +T.prototype._writev = function (chunks, cb) { + this.push('V(' + chunks.map((c) => String(c.chunk)).join('+') + ')'); + cb(); +}; +const t = Object.create(T.prototype); +T.call(t, {}); +t.on('data', (d) => console.log('t data', String(d))); +t.on('end', () => console.log('t end')); +t.write('a'); +t.cork(); +t.write('b'); +t.write('c'); +process.nextTick(() => { t.uncork(); t.end(); }); +function P(opts) { PassThrough.call(this, opts); } +Object.setPrototypeOf(P.prototype, PassThrough.prototype); +Object.setPrototypeOf(P, PassThrough); +const p = Object.create(P.prototype); +P.call(p, { writev(chunks, cb) { console.log('p writev', chunks.length, this === p); cb(); } }); +p.on('data', (d) => console.log('p data', String(d))); +p.cork(); +p.write('x'); +p.write('y'); +p.uncork(); +p.write('z'); diff --git a/tests/corpus/7462-transform-writev.ts b/tests/corpus/7462-transform-writev.ts new file mode 100644 index 000000000..ca079b3c9 --- /dev/null +++ b/tests/corpus/7462-transform-writev.ts @@ -0,0 +1,109 @@ +// Typed writev on Transform and PassThrough: an options writev with +// decodeStrings: false and flush, a Transform subclass's _writev, a +// batch completing with an error, and object-mode entries whose +// encoding is declared as a string or as Node's string | null | +// undefined. +import { Transform, PassThrough, Writable } from "node:stream"; +type WritevChunks = Array<{ chunk: any; encoding: BufferEncoding }>; +type Cb = (error?: Error | null) => void; +type TransformCallback = (error?: Error | null, data?: any) => void; +const log: string[] = []; +const t = new Transform({ + decodeStrings: false, + transform(chunk: string, enc: BufferEncoding, cb: TransformCallback): void { + log.push(`transform ${chunk} ${enc}`); + cb(null, chunk); + }, + flush(cb: TransformCallback): void { + log.push("flush"); + cb(null, "!"); + }, + writev(chunks: WritevChunks, cb: Cb): void { + log.push(`writev ${chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")}`); + this.push(chunks.map((c) => String(c.chunk)).join("")); + cb(); + }, +}); +t.on("data", (d: Buffer) => log.push(`data ${d.toString()}`)); +t.on("end", () => log.push("end")); +t.on("finish", () => log.push("finish")); +t.on("close", () => { + console.log(log.join("\n")); + next(); +}); +t.write("one"); +t.cork(); +t.write("two", "latin1"); +t.write("three"); +t.uncork(); +t.end("four"); + +class Batcher extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + console.log("batcher transform", chunk.toString()); + cb(null, chunk); + } + _writev(chunks: Array<{ chunk: any; encoding: BufferEncoding }>, cb: Cb): void { + console.log("batcher writev", chunks.length); + this.push(Buffer.concat(chunks.map((c) => c.chunk as Buffer))); + cb(); + } +} + +function next(): void { + const b = new Batcher(); + b.on("data", (d: Buffer) => console.log("batcher data", d.toString())); + b.write("x"); + b.cork(); + b.write("y"); + b.write("z"); + process.nextTick(() => { + b.uncork(); + b.end(); + }); + b.on("finish", () => { + const p = new PassThrough({ + writev(chunks: WritevChunks, cb: Cb): void { + console.log("pt writev", chunks.length, String(chunks[0]!.chunk)); + cb(new Error("batch failed")); + }, + }); + p.on("error", (e: Error) => { + console.log("pt error", e.message); + objectEntries(); + }); + p.cork(); + p.write("1"); + p.write("2"); + p.uncork(); + }); +} + +function objectEntries(): void { + const declared = new Writable({ + objectMode: true, + write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); }, + writev(chunks: WritevChunks, cb: Cb): void { + console.log("string encodings", JSON.stringify(chunks.map((c) => c.chunk))); + cb(); + }, + }); + declared.write({ n: 0 }); + declared.cork(); + declared.write({ n: 1 }); + declared.write({ n: 2 }, () => {}); + declared.uncork(); + const nodeTyped = new Writable({ + objectMode: true, + write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); }, + writev(chunks: Array<{ chunk: any; encoding: string | null | undefined }>, cb: Cb): void { + console.log("node encodings", JSON.stringify(chunks.map((c) => [c.chunk, c.encoding === undefined ? "undefined" : c.encoding]))); + cb(); + }, + }); + nodeTyped.write({ n: 0 }); + nodeTyped.cork(); + nodeTyped.write({ n: 1 }); + nodeTyped.write({ n: 2 }, () => {}); + nodeTyped.uncork(); +} diff --git a/tests/corpus/7463-stream-option-values.ts b/tests/corpus/7463-stream-option-values.ts new file mode 100644 index 000000000..04f497b6b --- /dev/null +++ b/tests/corpus/7463-stream-option-values.ts @@ -0,0 +1,58 @@ +// Typed decodeStrings and defaultEncoding options whose values are known +// only at run time: decodeStrings === false keeps string writes, a known +// defaultEncoding is the write default as given and how push(string) +// decodes, null and utf8 are the default, an unknown name throws +// ERR_UNKNOWN_ENCODING before construction, and a subclass's super() +// options take them the same way. +import { Writable, Readable, Duplex } from "node:stream"; +type Cb = (error?: Error | null) => void; +const flags: boolean[] = JSON.parse("[false, true]"); +const encodings: Array = JSON.parse('["hex", "latin1", null, "UTF8"]'); +for (const decode of flags) { + const w = new Writable({ + decodeStrings: decode, + defaultEncoding: encodings[0] as BufferEncoding, + write(chunk: any, enc: string, cb: Cb): void { + console.log("write", decode, typeof chunk, enc, String(chunk)); + cb(); + }, + }); + w.write("6869"); +} +const r = new Readable({ defaultEncoding: encodings[1] as BufferEncoding, read(): void {} }); +r.push("é"); +r.push(null); +r.on("data", (d: Buffer) => console.log("readable", d.toString("hex"))); +const dflt = new Writable({ + defaultEncoding: encodings[2] as any, + write(chunk: Buffer, enc: string, cb: Cb): void { + console.log("null default", enc, chunk.toString()); + cb(); + }, +}); +dflt.write("ok"); +const upper = new Duplex({ + defaultEncoding: encodings[3] as BufferEncoding, + decodeStrings: flags[0], + read(): void {}, + write(chunk: any, enc: string, cb: Cb): void { + console.log("duplex", typeof chunk, enc); + cb(); + }, +}); +upper.write("x"); +try { + new Writable({ defaultEncoding: JSON.parse('"nope"') }); +} catch (e) { + console.log("bad", (e as Error & { code?: string }).code, (e as Error).message); +} +class Sink extends Writable { + constructor(decode: boolean, encoding: BufferEncoding) { + super({ decodeStrings: decode, defaultEncoding: encoding }); + } + _write(chunk: any, enc: string, cb: Cb): void { + console.log("sink", typeof chunk, enc); + cb(); + } +} +new Sink(flags[0]!, encodings[1] as BufferEncoding).write("y"); diff --git a/tests/corpus/7464-stream-option-values.mjs b/tests/corpus/7464-stream-option-values.mjs new file mode 100644 index 000000000..dfeb7a447 --- /dev/null +++ b/tests/corpus/7464-stream-option-values.mjs @@ -0,0 +1,28 @@ +// Option values known only at run time in JavaScript: decodeStrings and +// defaultEncoding read from an object, a write callback whose chunk is +// then a string, decodeStrings: 0 (only false turns decoding off), a +// defaultEncoding that is not a string, and a _construct assigned after +// construction, which Node's constructor never reads. +import { Writable, Readable } from "node:stream"; +const opts = { decode: false, enc: "base64" }; +const w = new Writable({ + decodeStrings: opts.decode, + defaultEncoding: opts.enc, + write(c, e, cb) { console.log("js", typeof c, e, String(c)); cb(); }, +}); +w.write("aGk="); +try { new Writable({ defaultEncoding: 5 }); } catch (e) { console.log("bad", e.code, e.message); } +const zero = new Writable({ decodeStrings: 0, write(c, e, cb) { console.log("zero", Buffer.isBuffer(c), e); cb(); } }); +zero.write("z"); +const late = new Writable({ write(chunk, enc, cb) { console.log("write", chunk.toString()); cb(); } }); +let ran = false; +late._construct = (cb) => { ran = true; cb(); }; +late.write("a"); +late.end(() => console.log("ended, construct ran:", ran)); +const r = new Readable({ read() {} }); +const order = []; +(order.push("receiver"), r)._construct = (order.push("value"), function (cb) { console.log("never"); cb(); }); +console.log(order.join(",")); +r.push("x"); +r.push(null); +r.on("data", (d) => console.log("data", d.toString())); diff --git a/tests/corpus/7465-transform-callback-values.ts b/tests/corpus/7465-transform-callback-values.ts new file mode 100644 index 000000000..e7003e894 --- /dev/null +++ b/tests/corpus/7465-transform-callback-values.ts @@ -0,0 +1,41 @@ +// A transform or flush callback passing a value that is not a Buffer or a +// string: the push reports Node's ERR_INVALID_ARG_TYPE through 'error' +// and destroys the stream, for option callbacks and _transform methods, +// while Buffer and string values take the typed path. +import { Transform } from "node:stream"; +type TransformCallback = (error?: Error | null, data?: any) => void; +const t = new Transform({ + transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + console.log("transform", chunk.toString()); + cb(null, 42); + }, +}); +t.on("error", (e: Error & { code?: string }) => console.log("error", e.name, e.code, e.message)); +t.on("data", (d: Buffer) => console.log("data", d.toString())); +t.on("close", () => console.log("close", t.destroyed)); +console.log("write", t.write("a")); +const f = new Transform({ + transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { cb(null, chunk); }, + flush(cb: TransformCallback): void { cb(null, { tail: true }); }, +}); +f.on("error", (e: Error & { code?: string }) => console.log("flush error", e.code, e.message)); +f.on("data", (d: Buffer) => console.log("flush data", d.toString())); +f.on("close", () => console.log("flush close")); +f.end("z"); +class Bad extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + cb(null, chunk.length); + } +} +const bad = new Bad(); +bad.on("error", (e: Error & { code?: string }) => console.log("error", e.code, e.message)); +bad.on("close", () => console.log("close")); +bad.write("abc"); +class Good extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + cb(null, chunk.toString().toUpperCase()); + } +} +const good = new Good(); +good.on("data", (d: Buffer) => console.log("good", d.toString())); +good.end("xyz"); diff --git a/tests/harness/corpus-costs/7460-transform-writev.mjs.json b/tests/harness/corpus-costs/7460-transform-writev.mjs.json new file mode 100644 index 000000000..f6c21fd3d --- /dev/null +++ b/tests/harness/corpus-costs/7460-transform-writev.mjs.json @@ -0,0 +1 @@ +{ "plain": 2266, "san": 2991 } diff --git a/tests/harness/corpus-costs/7461-transform-writev.cjs.json b/tests/harness/corpus-costs/7461-transform-writev.cjs.json new file mode 100644 index 000000000..b5868c90b --- /dev/null +++ b/tests/harness/corpus-costs/7461-transform-writev.cjs.json @@ -0,0 +1 @@ +{ "plain": 1828, "san": 2413 } diff --git a/tests/harness/corpus-costs/7462-transform-writev.ts.json b/tests/harness/corpus-costs/7462-transform-writev.ts.json new file mode 100644 index 000000000..f8fd66c2a --- /dev/null +++ b/tests/harness/corpus-costs/7462-transform-writev.ts.json @@ -0,0 +1 @@ +{ "plain": 2178, "san": 2875 } diff --git a/tests/harness/corpus-costs/7463-stream-option-values.ts.json b/tests/harness/corpus-costs/7463-stream-option-values.ts.json new file mode 100644 index 000000000..0f66a4266 --- /dev/null +++ b/tests/harness/corpus-costs/7463-stream-option-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1786, "san": 4192 } diff --git a/tests/harness/corpus-costs/7464-stream-option-values.mjs.json b/tests/harness/corpus-costs/7464-stream-option-values.mjs.json new file mode 100644 index 000000000..b609b128f --- /dev/null +++ b/tests/harness/corpus-costs/7464-stream-option-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 1678, "san": 4502 } diff --git a/tests/harness/corpus-costs/7465-transform-callback-values.ts.json b/tests/harness/corpus-costs/7465-transform-callback-values.ts.json new file mode 100644 index 000000000..1c67bb43f --- /dev/null +++ b/tests/harness/corpus-costs/7465-transform-callback-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1571, "san": 2074 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 4d5546ab6..669a3a0d6 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 6134dc4c8915c27b28b40ede5d5d673309a6d41f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:20:40 -0500 Subject: [PATCH 121/169] Run two-counter while loops as two-counter for loops --- packages/compiler/src/backend/llvm/emitter.ts | 5 +- .../compiler/src/ir/integer-loops.test.ts | 10 + packages/compiler/src/ir/integer-loops.ts | 19 +- .../src/ir/paired-while-loops.test.ts | 182 +++++++++++ .../compiler/src/ir/paired-while-loops.ts | 307 ++++++++++++++++++ .../corpus/7144-paired-while-counters.ts.json | 6 + tests/corpus/7144-paired-while-counters.ts | 244 ++++++++++++++ .../7144-paired-while-counters.ts.json | 1 + 8 files changed, 765 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/src/ir/paired-while-loops.test.ts create mode 100644 packages/compiler/src/ir/paired-while-loops.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json create mode 100644 tests/corpus/7144-paired-while-counters.ts create mode 100644 tests/harness/corpus-costs/7144-paired-while-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index efe6097df..dd114b525 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -92,6 +92,7 @@ import { type IntegerRanges, } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; +import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; import { resolveSelfCaptures } from "../../ir/self-captures.js"; import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js"; @@ -527,7 +528,9 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { resolveSelfCaptures( hoistFinalFieldReads( freezeInitOnlyGlobals( - holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), ), ), ), diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index b6ac27e3a..2cca43a00 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -402,6 +402,16 @@ test("paired counters closing in on each other run as integers", () => { guarded: false, range: { min: 0, max: 9 }, }); + // `hi > lo` and `hi >= lo` are `lo < hi` and `lo <= hi`. + for (const op of [">", ">="] as const) { + const reversed = paired(number(0), number(9)); + reversed.cond = { kind: "bin", op, left: ref("hi"), right: ref("lo"), type: BOOL, loc }; + expect(matchIntegerPairedForLoop(reversed, vars)).toMatchObject({ + low: { localId: "lo", step: 1 }, + high: { localId: "hi", step: -1 }, + inclusive: op === ">=", + }); + } // A start without a known range is tested at run time. expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ high: { startGuarded: true }, diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index f54f6618b..d6942718a 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -382,9 +382,9 @@ export function matchIntegerCountedForLoop( * * The first counter only grows and the second only shrinks, by positive * literal steps, the body writes neither, and the loop runs while the first - * is below (or at most) the second. Every body entry therefore holds both - * between the first start and the second start: exact integers when the - * starts are. A start without a proven integer range is tested at run time + * is below (or at most) the second, written either way round (`hi > lo`). + * Every body entry therefore holds both between the first start and the + * second start: exact integers when the starts are. A start without a proven integer range is tested at run time * on a versioned loop, as a counted loop's is. */ export interface IntegerPairedCounter { localId: string; @@ -412,10 +412,13 @@ export function matchIntegerPairedForLoop( cond = stmt.cond; if (init?.kind !== "block" || init.body.length !== 2) return null; if (update?.kind !== "block" || update.body.length !== 2) return null; - if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; - if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; - const lowId = cond.left.localId, - highId = cond.right.localId; + if (cond?.kind !== "bin" || cond.left.kind !== "varRef" || cond.right.kind !== "varRef") + return null; + const below = cond.op === "<" || cond.op === "<="; + if (!below && cond.op !== ">" && cond.op !== ">=") return null; + // `hi > lo` is `lo < hi`. + const lowId = below ? cond.left.localId : cond.right.localId, + highId = below ? cond.right.localId : cond.left.localId; if (lowId === highId) return null; const counter = (id: string): IntegerPairedCounter | null => { const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); @@ -485,7 +488,7 @@ export function matchIntegerPairedForLoop( return { low, high, - inclusive: cond.op === "<=", + inclusive: cond.op === "<=" || cond.op === ">=", guarded, // A loop whose starts already fail the condition never enters the body. range: { diff --git a/packages/compiler/src/ir/paired-while-loops.test.ts b/packages/compiler/src/ir/paired-while-loops.test.ts new file mode 100644 index 000000000..e0a69afa3 --- /dev/null +++ b/packages/compiler/src/ir/paired-while-loops.test.ts @@ -0,0 +1,182 @@ +import { expect, test } from "vitest"; +import { + BOOL, + F64, + arrayOf, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { runPairedWhileLoopsAsFor } from "./paired-while-loops.js"; + +const loc = { file: "paired-while-loops.ts", start: 0, end: 0 }; +const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const array = { kind: "varRef", localId: "a", type: arrayOf(F64), loc } as const; +const element = (index: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "arrayGet", arr: array, index, type: F64, loc }, + loc, +}); +const step = (id: string, op: "+" | "-"): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(1), type: F64, loc }, + loc, +}); +const declare = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc }); +const number_ = (id: string): IrLocal => ({ + id, + name: id.split(".")[0]!, + type: F64, + mutable: true, +}); + +function whileLoop( + body: IrStmt[] = [element(ref("lo.0")), element(ref("hi.0"))], + updates: IrStmt[] = [step("lo.0", "+"), step("hi.0", "-")], +): IrStmt & { kind: "while" } { + return { + kind: "while", + cond: { kind: "bin", op: "<", left: ref("lo.0"), right: ref("hi.0"), type: BOOL, loc }, + body: [...body, ...updates], + loc, + }; +} + +function program(body: IrStmt[], extra: Partial = {}): IrModule { + const fn: IrFunction = { + name: "f", + params: [{ localId: "n.0", name: "n", type: F64 }], + returnType: F64, + locals: [ + number_("n.0"), + number_("lo.0"), + number_("hi.0"), + { id: "a", name: "a", type: arrayOf(F64), mutable: false }, + ], + body, + loc, + ...extra, + }; + return { functions: [fn], globals: [] } as unknown as IrModule; +} + +const rewritten = (mod: IrModule): IrStmt[] => runPairedWhileLoopsAsFor(mod).functions[0]!.body; + +test("a two-counter while loop runs as a for loop over new counters", () => { + const mod = program([declare("lo.0", number(0)), declare("hi.0", ref("n.0")), whileLoop()]); + const out = runPairedWhileLoopsAsFor(mod).functions[0]!; + // The declarations stay; the loop's counters are new locals that start + // from them, and the trailing updates are the for loop's update. + expect(out.body.slice(0, 2)).toEqual(mod.functions[0]!.body.slice(0, 2)); + expect(out.body[2]).toMatchObject({ + kind: "for", + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo.1", init: { kind: "varRef", localId: "lo.0" } }, + { kind: "varDecl", localId: "hi.1", init: { kind: "varRef", localId: "hi.0" } }, + ], + }, + cond: { op: "<", left: { localId: "lo.1" }, right: { localId: "hi.1" } }, + update: { kind: "block", body: [{ localId: "lo.1" }, { localId: "hi.1" }] }, + body: [ + { expr: { kind: "arrayGet", index: { localId: "lo.1" } } }, + { expr: { kind: "arrayGet", index: { localId: "hi.1" } } }, + ], + }); + expect(out.locals.map((l) => l.id)).toEqual(["n.0", "lo.0", "hi.0", "a", "lo.1", "hi.1"]); +}); + +test("parameters, reversed comparisons, labels and either update order qualify", () => { + const loop = whileLoop(undefined, [step("hi.0", "-"), step("lo.0", "+")]); + loop.cond = { kind: "bin", op: ">=", left: ref("hi.0"), right: ref("lo.0"), type: BOOL, loc }; + loop.labels = ["outer"]; + const mod = program([declare("lo.0", number(0)), loop], { + params: [{ localId: "hi.0", name: "hi", type: F64 }], + }); + expect(rewritten(mod)[1]).toMatchObject({ + kind: "for", + labels: ["outer"], + cond: { op: ">=", left: { localId: "hi.1" }, right: { localId: "lo.1" } }, + update: { body: [{ localId: "hi.1" }, { localId: "lo.1" }] }, + }); +}); + +test("loops the for form would change, or whose counters are used elsewhere, stay", () => { + const decls = [declare("lo.0", number(0)), declare("hi.0", ref("n.0"))]; + const kept = (body: IrStmt[], extra: Partial = {}): void => { + const mod = program(body, extra); + expect(runPairedWhileLoopsAsFor(mod)).toBe(mod); + }; + // A continue that binds to the loop skips the trailing updates. + kept([...decls, whileLoop([{ kind: "continue", loc }, element(ref("lo.0"))])]); + const labeled = whileLoop([{ kind: "continue", label: "outer", loc }, element(ref("lo.0"))]); + labeled.labels = ["outer"]; + kept([...decls, labeled]); + // A counter read after the loop, written before it, or declared twice. + kept([...decls, whileLoop(), { kind: "return", value: ref("lo.0"), loc }]); + kept([...decls, step("hi.0", "-"), whileLoop()]); + kept([...decls, declare("lo.0", number(1)), whileLoop()]); + // A declaration outside an enclosing loop: a later entry would resume + // from the values the previous run left. + kept([ + ...decls, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [whileLoop()], + loc, + }, + ]); + // A parameter counter inside another loop, for the same reason. + const outerLoop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [whileLoop()], + loc, + }; + kept([outerLoop], { + params: [ + { localId: "lo.0", name: "lo", type: F64 }, + { localId: "hi.0", name: "hi", type: F64 }, + ], + }); + // A counter that was never declared, or that this function captures. + kept([declare("lo.0", number(0)), whileLoop()]); + kept([...decls, whileLoop()], { captures: [{ localId: "hi.0", name: "hi", type: F64 }] }); + // The body writes a counter, a counter is boxed, or no counter is an index. + kept([...decls, whileLoop([element(ref("lo.0")), step("hi.0", "-")])]); + const boxed = program([...decls, whileLoop()]); + boxed.functions[0]!.locals[2] = { ...number_("hi.0"), boxed: true }; + expect(runPairedWhileLoopsAsFor(boxed)).toBe(boxed); + kept([...decls, whileLoop([])]); +}); + +test("a continue in a nested loop keeps the outer loop eligible", () => { + const inner: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: false, type: BOOL, loc }, + body: [{ kind: "continue", loc }], + loc, + }; + // Literal starts have known ranges, so the loop needs no start test and + // may hold a nested loop. + const mod = program([ + declare("lo.0", number(0)), + declare("hi.0", number(9)), + whileLoop([inner, element(ref("lo.0"))]), + ]); + expect(rewritten(mod)[2]).toMatchObject({ kind: "for", body: [{ kind: "while" }, {}] }); + // A start without a known range is tested at run time, on a loop that + // may not nest another. + const guarded = program([ + declare("lo.0", number(0)), + declare("hi.0", ref("n.0")), + whileLoop([inner, element(ref("lo.0"))]), + ]); + expect(runPairedWhileLoopsAsFor(guarded)).toBe(guarded); +}); diff --git a/packages/compiler/src/ir/paired-while-loops.ts b/packages/compiler/src/ir/paired-while-loops.ts new file mode 100644 index 000000000..e0a994418 --- /dev/null +++ b/packages/compiler/src/ir/paired-while-loops.ts @@ -0,0 +1,307 @@ +import { matchIntegerPairedForLoop } from "./integer-loops.js"; +import { analyzeIntegerRanges, type IntegerRange } from "./integer-ranges.js"; +import { + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; + +type WhileLoop = IrStmt & { kind: "while" }; + +/** Two-counter while loops run as two-counter for loops. + * + * let lo = a, hi = b; + * while (lo < hi) { ...; lo++; hi--; } + * + * runs as `for (let l = lo, h = hi; l < h; l++, h--) { ... }`, whose + * counters the backend stores as integers (matchIntegerPairedForLoop). The + * loop qualifies when: + * - its condition compares the two counters (`<`, `<=`, `>` or `>=`); + * - its body ends with one update of each counter; + * - no `continue` in the body binds to it (a for loop's `continue` runs the + * update, a while loop's skips the body's last statements); + * - the function mentions neither counter outside the loop, except in one + * declaration or as a parameter; + * - every entry starts from the declared or passed values: a declaration is + * an earlier statement of the loop's own statement list, which runs before + * each entry, and a loop over a parameter is in no other loop. + * + * The for loop's counters are new locals that start from the counters' + * values on entry, so the declarations stay where they are and nothing + * between them and the loop moves. The original counters are not read + * after the loop, so they need no final value. A loop that the paired match + * would refuse is left as it is. */ +export function runPairedWhileLoopsAsFor(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn) => { + const next = rewriteFunction(fn); + if (next !== fn) changed = true; + return next; + }); + return changed ? { ...mod, functions } : mod; +} + +/** The local a statement steps: `x++`, `x--` or `x = ...`. */ +function updatedLocal(s: IrStmt): string | null { + if (s.kind === "assign") return s.localId; + if (s.kind === "exprStmt" && s.expr.kind === "incDec") return s.expr.localId; + return null; +} + +/** The counters of a loop with the qualifying condition and trailing + * updates, or null. */ +function loopCounters(loop: WhileLoop, locals: ReadonlyMap): string[] | null { + const cond = loop.cond; + if (cond.kind !== "bin") return null; + if (cond.op !== "<" && cond.op !== "<=" && cond.op !== ">" && cond.op !== ">=") return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const counters = [cond.left.localId, cond.right.localId]; + if (counters[0] === counters[1]) return null; + const n = loop.body.length; + if (n < 2) return null; + const updated = [updatedLocal(loop.body[n - 2]!), updatedLocal(loop.body[n - 1]!)]; + if (!counters.every((id) => updated.includes(id))) return null; + for (const id of counters) { + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + } + return counters; +} + +function isLoop(s: IrStmt): boolean { + return s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf"; +} + +/** True when a `continue` in the body binds to the loop: an unlabeled one + * outside nested loops, or one that names a label of the loop. */ +function continuesLoop(loop: WhileLoop): boolean { + const labels = loop.labels ?? []; + let depth = 0; + let found = false; + const expr = (e: IrExpr): boolean => !found && everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (found) return false; + if (s.kind === "continue") { + if (s.label === undefined ? depth === 0 : labels.includes(s.label)) found = true; + return !found; + } + const nested = isLoop(s); + if (nested) depth++; + const ok = everyStmtChild(s, expr, stmt); + if (nested) depth--; + return ok; + }; + loop.body.every(stmt); + return found; +} + +/** Mentions of each id in `node`, in any field (a string literal equal to + * an id counts too, which only refuses more loops). */ +function countMentions( + node: unknown, + ids: ReadonlySet, + mentions: Map, +): void { + if (typeof node === "string") { + if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1); + } else if (Array.isArray(node)) { + for (const item of node) countMentions(item, ids, mentions); + } else if (node !== null && typeof node === "object") { + const record = node as Record; + for (const key in record) + if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions); + } +} + +/** A loop with the qualifying shape, the statements before it in its + * statement list, and whether it is inside another loop. */ +interface Candidate { + loop: WhileLoop; + counters: string[]; + before: readonly IrStmt[]; + nested: boolean; +} + +function findCandidates(fn: IrFunction, locals: ReadonlyMap): Candidate[] { + const candidates: Candidate[] = []; + let depth = 0; + const list = (body: readonly IrStmt[]): void => { + body.forEach((s, i) => { + if (s.kind === "while") { + const counters = loopCounters(s, locals); + if (counters !== null && !continuesLoop(s)) + candidates.push({ loop: s, counters, before: body.slice(0, i), nested: depth > 0 }); + } + visit(s); + }); + }; + // Statement lists inside expressions are not searched: their loops stay. + const visit = (s: IrStmt): void => { + const loop = isLoop(s); + if (loop) depth++; + switch (s.kind) { + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + list(s.body); + break; + case "if": + list(s.then); + if (s.else_ !== null) list(s.else_); + break; + case "switch": + for (const c of s.cases) list(c.body); + break; + case "tryCatch": + list(s.tryBody); + if (s.catchBody !== null) list(s.catchBody); + if (s.finallyBody !== null) list(s.finallyBody); + break; + default: + break; + } + if (loop) depth--; + }; + list(fn.body); + return candidates; +} + +function rewriteFunction(fn: IrFunction): IrFunction { + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const candidates = findCandidates(fn, locals); + if (candidates.length === 0) return fn; + + const ids = new Set(); + for (const c of candidates) for (const id of c.counters) ids.add(id); + const total = new Map(); + countMentions(fn.body, ids, total); + const params = new Set(fn.params.map((p) => p.localId)); + // A counter this function reaches through its environment. + const outer = new Map(); + countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer); + + let ranges: ReadonlyMap | null = null; + const used = new Set(fn.locals.map((l) => l.id)); + const added: IrLocal[] = []; + const rewritten = new Map>(); + for (const { loop, counters, before, nested } of candidates) { + const inside = new Map(); + countMentions(loop, new Set(counters), inside); + // Each counter's start: its declaration's initializer, or null for a + // parameter. + const starts = new Map(); + for (const id of counters) { + if ((outer.get(id) ?? 0) !== 0) break; + const elsewhere = (total.get(id) ?? 0) - (inside.get(id) ?? 0); + if (params.has(id)) { + if (elsewhere === 0 && !nested) starts.set(id, null); + continue; + } + const decl = before.find((s) => s.kind === "varDecl" && s.localId === id); + if (elsewhere === 1 && decl?.kind === "varDecl" && decl.init !== null) + starts.set(id, decl.init); + } + if (starts.size !== counters.length) continue; + const renamed = new Map(); + const fresh: IrLocal[] = []; + for (const id of counters) { + const local = locals.get(id)!; + let k = 0; + while (used.has(`${local.name}.${k}`)) k++; + const next = `${local.name}.${k}`; + used.add(next); + renamed.set(id, next); + fresh.push({ ...local, id: next }); + } + const candidate = pairedForLoop(loop, renamed); + const forLocals = new Map(locals); + for (const local of fresh) forLocals.set(local.id, local); + // A start reads its counter on entry, which holds the initializer's + // value, with that value's range. + ranges ??= analyzeIntegerRanges(fn); + const startRanges = new Map(); + if (candidate.init?.kind === "block") + for (const decl of candidate.init.body) { + if (decl.kind !== "varDecl" || decl.init?.kind !== "varRef") continue; + const init = starts.get(decl.init.localId); + if (!init) continue; + const literal = + init.kind === "numLit" && Number.isSafeInteger(init.value) && !Object.is(init.value, -0); + startRanges.set( + decl.init, + literal ? { min: init.value, max: init.value } : (ranges.get(init) ?? null), + ); + } + if (matchIntegerPairedForLoop(candidate, forLocals, startRanges) === null) { + for (const local of fresh) used.delete(local.id); + continue; + } + rewritten.set(loop, renamed); + added.push(...fresh); + } + if (rewritten.size === 0) return fn; + + const expr = (e: IrExpr): IrExpr => mapExprChildren(e, expr, stmt); + const stmt = (s: IrStmt): IrStmt => { + const next = mapStmtChildren(s, expr, stmt); + const renamed = s.kind === "while" ? rewritten.get(s) : undefined; + // The body may hold rewritten loops of its own. + return renamed === undefined ? next : pairedForLoop(next as WhileLoop, renamed); + }; + return { ...fn, body: fn.body.map(stmt), locals: [...fn.locals, ...added] }; +} + +/** The for loop over the renamed counters: their starts read the original + * counters, and the body's two trailing updates become the update. */ +function pairedForLoop( + loop: WhileLoop, + renamed: ReadonlyMap, +): IrStmt & { kind: "for" } { + const expr = (e: IrExpr): IrExpr => { + const next = mapExprChildren(e, expr, stmt); + if (next.kind === "varRef" || next.kind === "incDec" || next.kind === "assignExpr") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } + return next; + }; + const stmt = (s: IrStmt): IrStmt => { + const next = mapStmtChildren(s, expr, stmt); + if ( + next.kind === "varDecl" || + next.kind === "assign" || + next.kind === "forOf" || + next.kind === "rethrow" + ) { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } + return next; + }; + const loc = loop.loc; + const n = loop.body.length; + const declarations: IrStmt[] = []; + for (const [id, next] of renamed) + declarations.push({ + kind: "varDecl", + localId: next, + init: { kind: "varRef", localId: id, type: F64, loc }, + loc, + }); + const result: IrStmt & { kind: "for" } = { + kind: "for", + init: { kind: "block", body: declarations, loc }, + cond: expr(loop.cond), + update: { kind: "block", body: loop.body.slice(n - 2).map(stmt), loc }, + body: loop.body.slice(0, n - 2).map(stmt), + loc, + }; + if (loop.labels !== undefined) result.labels = loop.labels; + return result; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json new file mode 100644 index 000000000..678cbd8b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7144-paired-while-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7144-paired-while-counters.ts b/tests/corpus/7144-paired-while-counters.ts new file mode 100644 index 000000000..2198d213f --- /dev/null +++ b/tests/corpus/7144-paired-while-counters.ts @@ -0,0 +1,244 @@ +// While loops with two counters closing in on each other (`lo++; hi--;` at +// the end of the body): reversals over arrays, typed arrays and strings, +// either comparison direction, inclusive bounds, larger steps, parameters as +// counters, early exits, unproven starts, and the shapes that keep the loop +// as it is (a continue, a counter read after the loop, a captured counter, +// a counter the body writes). +function reverse(a: number[], from: number, to: number): number[] { + let lo = from; + let hi = to; + while (lo < hi) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + lo++; + hi--; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +console.log(reverse(base(), -0, 2).join(","), reverse(base(), 7, -0).join(",")); + +// The parameters are the counters; the updates come in the other order. +function reverseRange(a: Float64Array, lo: number, hi: number): string { + while (hi > lo) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + hi -= 1; + ++lo; + } + return a.join(","); +} +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 0, 5)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 1, 3.5)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), -2, 2)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), NaN, 4), reverseRange(new Float64Array([1, 2]), 0, NaN)); + +function bytesReversed(text: string): string { + const bytes = new TextEncoder().encode(text); + let i = 0, + j = bytes.length - 1; + while (i < j) { + const t = bytes[i]!; + bytes[i] = bytes[j]!; + bytes[j] = t; + i++; + j--; + } + return new TextDecoder().decode(bytes); +} +console.log(bytesReversed("hello, world"), bytesReversed(""), bytesReversed("x")); + +// Unproven starts read through charCodeAt (which truncates its index). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + let lo = from; + let hi = to; + while (lo <= hi) { + out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + lo += 2; + hi -= 3; + } + return out.join(","); +} +const word = "abcdefghijkl"; +console.log(codes(word, 0, 11), "|", codes(word, 0.5, 9.25), "|", codes(word, 4, 4)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53), "|", codes(word, -3, 2), "|", codes(word, 5, 1)); + +// Negative zero starts stay negative zero. +function signs(text: string, from: number): string { + const out: string[] = []; + let lo = from; + let hi = text.length - 1; + while (lo < hi) { + out.push(`${Object.is(lo, -0)}:${text.charCodeAt(lo)}`); + lo++; + hi--; + } + let a = -0; + let b = 2; + while (a < b) { + out.push(`${Object.is(a, -0)}:${text.charCodeAt(b)}`); + a++; + b--; + } + return out.join(" "); +} +console.log(signs("abcd", -0), signs("abcd", 0)); + +function isPalindrome(s: string): boolean { + let i = 0; + let j = s.length - 1; + while (i < j) { + if (s.charCodeAt(i) !== s.charCodeAt(j)) return false; + i++; + j--; + } + return true; +} +console.log(isPalindrome("racecar"), isPalindrome("abca"), isPalindrome(""), isPalindrome("ab")); + +function firstMismatch(a: number[]): number { + let found = -1; + let lo = 0; + let hi = a.length - 1; + while (lo < hi) { + const left = a[lo]!; + if (left !== a[hi]!) { + found = lo; + break; + } + lo++; + hi--; + } + return found; +} +console.log(firstMismatch([1, 2, 3, 2, 1]), firstMismatch([1, 2, 3, 4, 2, 1]), firstMismatch([])); + +// A labeled jump to an outer loop leaves the inner loop from its middle. +function rows(grid: number[][]): string { + const out: string[] = []; + outer: for (const row of grid) { + let lo = 0; + let hi = row.length - 1; + while (lo < hi) { + const left = row[lo]!; + const right = row[hi]!; + if (left === 0) continue outer; + if (right === 0) break outer; + out.push(`${left}${right}`); + lo++; + hi--; + } + out.push("|"); + } + return out.join(" "); +} +console.log(rows([[1, 2, 3, 4], [0, 5, 6], [7, 8, 9, 1], [2, 3, 0, 4]])); + +// Repeated entries: the counters are declared inside an outer loop. Each +// pass reverses the prefix that ends at the index the first element holds, +// until the first element is 0, and the passes are counted. +function prefixReversals(values: number[]): number { + const copy = values.slice(); + let count = 0; + let end = copy[0]!; + while (end !== 0) { + let i = 0; + let j = end; + while (i < j) { + const t = copy[i]!; + copy[i] = copy[j]!; + copy[j] = t; + i++; + j--; + } + count++; + end = copy[0]!; + } + return count; +} +console.log( + prefixReversals([3, 1, 2, 0]), + prefixReversals([0, 1, 2]), + prefixReversals([4, 3, 2, 1, 0]), + prefixReversals([2, 0, 1]), +); + +// Holes and present undefined values move as they are. +const sparse: (number | undefined)[] = [1, , 3, undefined, 5]; +{ + let lo = 0; + let hi = sparse.length - 1; + while (lo < hi) { + const t = sparse[lo]; + sparse[lo] = sparse[hi]; + sparse[hi] = t; + lo++; + hi--; + } +} +console.log(sparse, 1 in sparse, 3 in sparse); + +// These keep the while loop: a continue skips the trailing updates, a +// counter is read after the loop, a closure captures a counter, and the +// body writes a counter. +function skipping(n: number): string { + const out: number[] = []; + let lo = 0; + let hi = n; + while (lo < hi) { + if (lo % 3 === 0) { + lo += 2; + continue; + } + out.push(lo, hi); + lo++; + hi--; + } + return out.join(","); +} +console.log(skipping(12)); + +function meet(n: number): string { + let lo = 0; + let hi = n; + while (lo < hi) { + lo++; + hi--; + } + return `${lo}:${hi}`; +} +console.log(meet(9), meet(10), meet(0.5)); + +function captured(n: number): string { + const seen: (() => number)[] = []; + let lo = 0; + let hi = n; + while (lo < hi) { + seen.push(() => lo * 100 + hi); + lo++; + hi--; + } + return seen.map((f) => f()).join(","); +} +console.log(captured(5)); + +function jumps(a: number[]): string { + const out: number[] = []; + let lo = 0; + let hi = a.length - 1; + while (lo < hi) { + out.push(a[lo]!, a[hi]!); + if (a[lo]! > 5) hi = lo; + lo++; + hi--; + } + return out.join(","); +} +console.log(jumps([1, 2, 9, 4, 5, 6, 7, 8])); diff --git a/tests/harness/corpus-costs/7144-paired-while-counters.ts.json b/tests/harness/corpus-costs/7144-paired-while-counters.ts.json new file mode 100644 index 000000000..935249de9 --- /dev/null +++ b/tests/harness/corpus-costs/7144-paired-while-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4600 } From d027786c8050e35d239f0a5188fdbe5df46dc17a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:29:34 -0500 Subject: [PATCH 122/169] Convert Buffers and DataViews to BigInt through their own text BigInt of byte storage joined the elements with "," for every kind, so a Buffer, whose text is its UTF-8 decoding, and a DataView, whose text is "[object DataView]", converted as typed arrays. BigInt now parses the value's ToString, which the runtime value decides, as String does. This also covers a Buffer held in a Uint8Array slot. Corpus 7403 adds Buffers, a DataView, and a Buffer in a typed array slot. Refs #1647 --- .../src/frontend/lowering/lower-calls.ts | 14 +++++++------- tests/corpus/7403-bigint-of-typed-arrays.mjs | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 8424f28c4..e44f0a2bc 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2400,15 +2400,15 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc }; - // A typed array converts through ToPrimitive to its elements joined with - // ",", which BigInt then parses: one element reads as that element, none - // as 0n, and several throw the SyntaxError for the joined text. + // Byte storage converts through ToPrimitive, which BigInt then parses: a + // typed array's text is its elements joined with "," (one element reads + // as that element, none as 0n, several throw the SyntaxError for the + // joined text), a Buffer's is its UTF-8 decoding and a DataView's is + // "[object DataView]". The runtime value tells them apart, as for String. if (arg.type.kind === "bytes") { const text: IrExpr = { - kind: "bytesIntrinsic", - method: "join", - receiver: arg, - args: [{ kind: "strLit", value: ",", type: STRING, loc }], + kind: "toString", + operand: { kind: "dynFrom", value: arg, type: DYN, loc }, type: STRING, loc, }; diff --git a/tests/corpus/7403-bigint-of-typed-arrays.mjs b/tests/corpus/7403-bigint-of-typed-arrays.mjs index 77ef0cd2e..9cf813d9a 100644 --- a/tests/corpus/7403-bigint-of-typed-arrays.mjs +++ b/tests/corpus/7403-bigint-of-typed-arrays.mjs @@ -2,7 +2,9 @@ // "," and parses that text, as in Node: one element reads as its value, // an empty array as 0n, and anything else throws the SyntaxError for the // joined text. Every element kind, negative and clamped values, float -// formatting, views and the results in arithmetic. +// formatting, views and the results in arithmetic. A Buffer converts +// through its UTF-8 text instead, also when a typed array slot holds it, +// and a DataView through "[object DataView]". const one = [ new Int8Array([-5]), @@ -44,3 +46,17 @@ for (let i = 0; i < 100; i++) { total += BigInt(window) * 3n; } console.log("window", BigInt(window), total, BigInt(bytes.subarray(2)) ** 2n); + +function convert(label, make) { + try { + console.log(label, BigInt(make())); + } catch (e) { + console.log(label, e instanceof SyntaxError, e.message); + } +} +convert("buffer", () => Buffer.from("12")); +convert("spaced buffer", () => Buffer.from(" -7 ")); +convert("empty buffer", () => Buffer.alloc(0)); +convert("fraction buffer", () => Buffer.from("1.5")); +convert("data view", () => new DataView(new ArrayBuffer(2))); +for (const view of [new Uint8Array([5]), Buffer.from("64")]) console.log("slot", BigInt(view)); From 37ea2c9ce7d5c8f11ffab42c3ac79daa71b8e321 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:31:26 -0700 Subject: [PATCH 123/169] Keep arrays stored through a typed global view native A fresh array literal written to a checked-dynamic slot is built as a dynamic array. On a member of a typed view of the global object (`(globalThis as typeof globalThis & { cache?: number[] }).cache ??= []`) the program reads it back through that declaration, and each typed read extracted a new native copy. `cache().push(x)` was lost, and two reads were never the same array. Node keeps one array. When the literal is the right side of a write (`=`, `??=`, `||=`) to a member of the global object, and the member is declared as a native array, build the native array and store it boxed. Every typed read then extracts that same array, and untyped reads see it through its live capsule. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 46 +++++++++++++++++++ .../7366-typed-global-view-arrays.ts.json | 6 +++ tests/corpus/7366-typed-global-view-arrays.ts | 32 +++++++++++++ .../7366-typed-global-view-arrays.ts.json | 1 + 4 files changed, 85 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json create mode 100644 tests/corpus/7366-typed-global-view-arrays.ts create mode 100644 tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4c417e86e..554b703e9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10609,6 +10609,38 @@ export class Lowerer { /** Lower for a known destination, then apply ordinary value coercion. * Fresh arrays and records use the destination layout directly. */ + /** `value` is the right side of a write to a member of a typed view of the + * global object (`(globalThis as typeof globalThis & { k?: T }).k = value`, + * also through `??=` and `||=`). */ + private globalViewSlot(value: ts.Expression): boolean { + const write = value.parent; + if ( + !ts.isBinaryExpression(write) || + write.right !== value || + (write.operatorToken.kind !== ts.SyntaxKind.EqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) + ) + return false; + let target = write.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) + return false; + const receiver = this.typeOf(target.expression); + const isGlobal = (type: ts.Type): boolean => { + const symbol = type.getSymbol(); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "globalThis" && + symbol.declarations.length === 0 + ); + }; + return ( + isGlobal(receiver) || + (receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal)) + ); + } + lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr { // These wrappers do not change the value or its inferred type. Keep a // fresh literal visible to destination-directed construction. Assertions @@ -10674,6 +10706,20 @@ export class Lowerer { (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { + // A checked slot that the program declares as a native array (a + // member of a typed view of the global object, `typeof globalThis & + // { cache?: number[] }`) is read back through that declaration. Build + // the native array and store it boxed: every typed read then extracts + // that same array (a dynamic literal would be copied on each read, + // losing writes such as push). + const slotType = this.globalViewSlot(x) ? this.checker.getContextualType(x) : undefined; + const declared = slotType && this.checker.getNonNullableType(slotType); + const native = + declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) + ? this.mapTypeOf(declared) + : null; + if (native?.kind === "array") + return this.coerceInto(node, this.lowerArrayLiteral(x, native), DYN); return { kind: "dynArrLit", elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..49da80b55 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7366-typed-global-view-arrays.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7366-typed-global-view-arrays.ts b/tests/corpus/7366-typed-global-view-arrays.ts new file mode 100644 index 000000000..3b24620d5 --- /dev/null +++ b/tests/corpus/7366-typed-global-view-arrays.ts @@ -0,0 +1,32 @@ +// Arrays stored on the global object through a typed view are the same +// arrays every typed read returns: writes through one read (push, index +// assignment, sort) are visible through the next, as in Node. +type Store = typeof globalThis & { __exampleCache?: number[]; __exampleNames?: string[] }; + +function cache(): number[] { + const g = globalThis as Store; + g.__exampleCache ??= [1]; + return g.__exampleCache; +} + +cache().push(2); +cache().push(3); +cache()[0] = 10; +console.log(cache().length, cache().join(","), cache() === cache()); + +const view = globalThis as Store; +view.__exampleNames = ["b", "a"]; +const names = view.__exampleNames; +names.push("c"); +names.sort(); +console.log(view.__exampleNames.join(","), names === view.__exampleNames); + +// the same array seen through an untyped read +const raw = (globalThis as unknown as Record)["__exampleCache"]; +console.log(Array.isArray(raw), JSON.stringify(raw), raw === cache()); + +// a literal of another element type stays its own value +const counts = globalThis as typeof globalThis & { __exampleCounts?: number[][] }; +counts.__exampleCounts = [[1], [2, 3]]; +counts.__exampleCounts[1]!.push(4); +console.log(JSON.stringify(counts.__exampleCounts)); diff --git a/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } From 390ac6f32691f75bc15ccfd4469ac1904e4ad044 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:27:16 -0700 Subject: [PATCH 124/169] Call observed class methods natively while their slot is unchanged Any computed write with a key TypeScript cannot know (zod's `curr[el] = ...` in ZodError.js, for one) puts every class method name in the method census. Every typed call of such a method then went through the observable prototype slot: a dynamic call with boxed arguments, followed by a checked conversion of the result (for a `Product[]` result, a deep walk of every element). Bundling untyped JavaScript made unrelated typed code several times slower per call. The prototype data now keeps each observed method's original value in a global. A typed, non-virtual call of a TypeScript method still reads the slot first, as before, and calls the method natively when the slot holds that original value. A replacement, on the prototype or as an own property, still takes the dynamic call. Virtual calls and methods declared in unchecked JavaScript keep the dynamic call. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-callbacks.ts | 39 ++++++++++ .../src/frontend/lowering/class-prototypes.ts | 13 +++- .../src/frontend/lowering/lower-calls.ts | 15 ++++ .../src/frontend/lowering/lower-classes.ts | 4 + .../test/observed-method-guard.test.ts | 77 +++++++++++++++++++ .../main.ts.json | 7 ++ .../main.ts | 59 ++++++++++++++ .../untyped.js | 15 ++++ .../main.ts.json | 1 + 9 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/observed-method-guard.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json create mode 100644 tests/corpus/7365-class-methods-untyped-computed-writes/main.ts create mode 100644 tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js create mode 100644 tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index b51158022..0bda0068e 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -164,6 +164,7 @@ export function classCallbackCall( fallback: IrExpr, loc: SrcLoc, calleeName = name, + original?: { originalId: () => string | undefined; directCall: () => IrExpr }, ): IrExpr { const bag = lowerer.declareHiddenLocal("%callbackBag", DYN); const present = lowerer.declareHiddenLocal("%callbackPresent", BOOL); @@ -235,6 +236,44 @@ export function classCallbackCall( answer = fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); } + // Read after observedSlot built the prototype data and its originals. + const originalId = observed ? original?.originalId() : undefined; + if (original && originalId !== undefined) { + // The slot still holds the method's original value: calling it is + // calling the method, so the call stays native (no boxed arguments, + // dynamic call or checked result). A replacement takes the dynamic call. + const unchanged: IrExpr = { + kind: "libCall", + fn: "dyn.sameValue", + args: [varRef(callback.id, DYN, loc), varRef(originalId, DYN, loc)], + type: BOOL, + loc, + }; + answer = + fallback.type.kind === "void" + ? { + kind: "ternary", + cond: unchanged, + then: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: original.directCall(), loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: call, + type: DYN, + loc, + } + : { + kind: "ternary", + cond: unchanged, + then: original.directCall(), + else_: answer, + type: answer.type, + loc, + }; + } return { kind: "seqExpr", stmts: [ diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 2a05cf977..3ecdd84aa 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -207,12 +207,21 @@ export function classPrototypeData( // promise-returning signature) gets no data slot. const boxedValue = lowerer.coerceToExpected(compiled, DYN); if (boxedValue.type.kind !== "dyn") continue; + const originalId = `%g.${info.def.prototypeDataHelper}.original.${method}`; + lowerer.globalsList.push({ + id: originalId, + name: `${info.def.jsName ?? info.def.name}.prototype.${method}.original`, + type: DYN, + mutable: true, + }); + info.prototypeOriginals ??= new Map(); + info.prototypeOriginals.set(method, originalId); const descriptor: IrExpr = { kind: "dynObjLit", fields: [ { key: { kind: "strLit", value: "value", type: STRING, loc }, - value: boxedValue, + value: varRef(originalId, DYN, loc), }, { key: { kind: "strLit", value: "writable", type: STRING, loc }, @@ -227,6 +236,8 @@ export function classPrototypeData( loc, }; const init = helper.body[0]!; + if (init.kind === "if") + init.then.push({ kind: "assign", localId: originalId, value: boxedValue, loc }); if (init.kind === "if") init.then.push({ kind: "exprStmt", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 14d94ad24..493d979f5 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10195,6 +10195,20 @@ export function lowerObjectMethodCall( type: found.sig.ret, loc, }; + // The declaring class's original value, built with the observed slot. + // An override below (a virtual call) keeps the dynamic call: a slot + // reassigned to an ancestor's method must not reach the override. So do + // methods declared in unchecked JavaScript, whose results are inferred. + const declarerFile = found.declarer.decl ? ts.getSourceFileOfNode(found.declarer.decl) : null; + const original = + virtual || + !declarerFile || + (isJsSourceFile(declarerFile) && !lowerer.checkedJsFile(declarerFile)) + ? undefined + : { + originalId: (): string | undefined => found.declarer.prototypeOriginals?.get(method), + directCall: (): IrExpr => fallback, + }; const result = classCallbackCall( lowerer, value, @@ -10203,6 +10217,7 @@ export function lowerObjectMethodCall( fallback, loc, ts.getTextOfNode(access), + original, ); return { kind: "seqExpr", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6dd0124bd..29afbb4fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -478,6 +478,10 @@ export interface ClassInfo { prototypeMethodValues?: Map; prototypeReflectionReady?: true; prototypeObservedMethodsReady?: true; + /** The globals holding each observed method's original prototype value + * (set when the prototype data is built), by method name. A typed call + * whose slot still holds that value calls the method natively. */ + prototypeOriginals?: Map; prototypeReflectionRefusal?: string; localClass?: { context: FnCtx | null; diff --git a/packages/compiler/test/observed-method-guard.test.ts b/packages/compiler/test/observed-method-guard.test.ts new file mode 100644 index 000000000..5964c482b --- /dev/null +++ b/packages/compiler/test/observed-method-guard.test.ts @@ -0,0 +1,77 @@ +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, deserializeModule, validateModule } from "../src/index.js"; + +const dirs: string[] = []; + +afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +type IrRecord = Record; + +function recordsOf(value: unknown, out: IrRecord[] = []): IrRecord[] { + if (value === null || typeof value !== "object") return out; + if (Array.isArray(value)) { + for (const item of value) recordsOf(item, out); + return out; + } + const record = value as IrRecord; + out.push(record); + for (const child of Object.values(record)) recordsOf(child, out); + return out; +} + +// An untyped computed write puts every method name in the census, so typed +// calls read the observable slot. While the slot holds the declared method, +// the call is the native method call, not a dynamic call of the slot. +test("typed calls of an observed method call it natively while its slot is unchanged", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-")); + dirs.push(dir); + const entry = join(dir, "main.ts"); + const outDir = join(dir, ".scriptc"); + const outPath = join(outDir, "main.ir.json"); + await writeFile( + join(dir, "untyped.js"), + "export function put(target, key, value) { target[key] = value; }\n", + ); + await writeFile( + entry, + [ + 'import { put } from "./untyped.js";', + "class Repo {", + " items: number[] = [];", + " total(): number { let s = 0; for (const n of this.items) s += n; return s; }", + "}", + "const repo = new Repo();", + "repo.items.push(1, 2);", + "put({}, String(Math.random()), 1);", + "export function sum(r: Repo): number { return r.total(); }", + "console.log(sum(repo));", + "", + ].join("\n"), + ); + const result = await compile(entry, { outDir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const module = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(module)).toEqual([]); + const sum = module.functions.find((fn) => fn.name.endsWith("sum")); + expect(sum).toBeDefined(); + const records = recordsOf(sum); + const guards = records.filter( + (record) => + record.kind === "ternary" && + (record.cond as IrRecord | undefined)?.kind === "libCall" && + (record.cond as IrRecord).fn === "dyn.sameValue", + ); + expect(guards).toHaveLength(1); + const then = guards[0]!.then as IrRecord; + expect(then.kind).toBe("call"); + expect(String(then.callee)).toMatch(/Repo\.total$/); + expect(module.globals?.some((global) => global.name === "Repo.prototype.total.original")).toBe( + true, + ); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json new file mode 100644 index 000000000..58cdacb62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js", + "/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts new file mode 100644 index 000000000..f7449f1af --- /dev/null +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts @@ -0,0 +1,59 @@ +// Typed classes next to untyped computed writes. Calls through a typed +// receiver keep the method while its prototype slot holds the declared +// method, and observe a replacement made by untyped code (on the prototype +// or as an own property of one instance) exactly as Node does. +import { patch, tally, unpatch } from "./untyped.js"; + +class Repository { + private items = new Map(); + add(name: string, cents: number): number { + this.items.set(name, cents); + return this.items.size; + } + list(): string[] { + return [...this.items.keys()]; + } + total(): number { + let sum = 0; + for (const cents of this.items.values()) sum += cents; + return sum; + } +} + +const words: string[] = ["list", "add", "list", "total"]; +console.log(JSON.stringify(tally(words))); + +const repo = new Repository(); +repo.add("keyboard", 12900); +repo.add("mouse", 4900); +let sum = 0; +for (let i = 0; i < 1000; i++) sum += repo.total(); +console.log(repo.list().join(","), sum); + +// untyped code replaces the method on the prototype +const proto: unknown = Object.getPrototypeOf(repo); +patch(proto, "total", function (this: Repository): number { + return -1; +}); +console.log(repo.total(), new Repository().total()); +unpatch(proto, "total"); +try { + repo.total(); +} catch (error) { + console.log("after delete:", error instanceof TypeError); +} + +// an own replacement on one instance shadows the prototype for that instance only +const other = new Repository(); +other.add("cable", 900); +patch(other, "list", () => ["shadowed"]); +console.log(other.list().join(","), repo.list().join(",")); +unpatch(other, "list"); +console.log(other.list().join(",")); + +// restoring the original value through the prototype brings back the method +const saved: unknown = (proto as Record)["list"]; +patch(proto, "list", () => ["patched"]); +console.log(repo.list().join(",")); +patch(proto, "list", saved); +console.log(repo.list().join(",")); diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js b/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js new file mode 100644 index 000000000..3c0d20189 --- /dev/null +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js @@ -0,0 +1,15 @@ +// Untyped helpers with computed writes whose keys TypeScript cannot know. +// They put every class method name in play for the method census. +export function tally(words) { + const counts = {}; + for (const word of words) counts[word] = (counts[word] || 0) + 1; + return counts; +} + +export function patch(target, name, impl) { + target[name] = impl; +} + +export function unpatch(target, name) { + delete target[name]; +} diff --git a/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json b/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json new file mode 100644 index 000000000..0347bbd22 --- /dev/null +++ b/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3400 } From 0314d9db3e0168cf3279995f979d67a610a32e08 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:52:25 -0500 Subject: [PATCH 125/169] Dispatch checked-dynamic method calls on method ids --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/dynamic-methods.json | 6 + .../runtime/workloads/dynamic-methods.mjs | 36 +++ packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/emit-module.ts | 2 + packages/compiler/src/backend/llvm/emitter.ts | 24 ++ .../compiler/src/backend/llvm/expr-dynamic.ts | 8 +- packages/compiler/src/backend/llvm/js-ic.ts | 2 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_bytes_dyn.c | 42 +-- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 22 +- packages/runtime/src/scr_dyn_invoke.c | 276 ++++++++++-------- packages/runtime/src/scr_dyn_methods.h | 160 ++++++++++ packages/runtime/src/scr_dyn_methods.test.ts | 10 + packages/runtime/src/scr_worker_events.c | 40 +-- scripts/gen-dyn-methods.mjs | 101 +++++++ scripts/zero-cost-compare.mjs | 4 +- .../self-hosting-native-toolchain.test.ts | 2 +- 25 files changed, 572 insertions(+), 188 deletions(-) create mode 100644 benchmarks/runtime/workloads/dynamic-methods.json create mode 100644 benchmarks/runtime/workloads/dynamic-methods.mjs create mode 100644 packages/runtime/src/scr_dyn_methods.h create mode 100644 packages/runtime/src/scr_dyn_methods.test.ts create mode 100644 scripts/gen-dyn-methods.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index e177e36c0..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -12,6 +12,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `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 | | `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-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json new file mode 100644 index 000000000..fa27f4ea8 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-methods.mjs", + "args": [ + "300000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs new file mode 100644 index 000000000..bbf217701 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.mjs @@ -0,0 +1,36 @@ +// Untyped JavaScript calling builtin methods on values it never declares +// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array +// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number +// toFixed, and calls of an object's own methods. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "300000"); +const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]; +const counts = new Map(); +const seen = new Set(); +const stack = []; +const recent = []; +const tally = { + total: 0, + add(value) { + this.total += value; + return this.total; + }, +}; +let checksum = 0; +for (let i = 0; i < rounds; i++) { + const word = words[i % words.length]; + const head = word.slice(0, 3).toUpperCase(); + const key = head.padStart(4, "-"); + if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1; + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 97); + if (seen.has(i % 89)) checksum += 2; + stack.push(i); + if (stack.length > 32) checksum += stack.pop() % 3; + recent.push(word); + if (recent.length > 4) recent.shift(); + if (recent.includes("gamma")) checksum += recent.indexOf("gamma"); + checksum += tally.add(i % 5) % 11; + if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0; +} +console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total); diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index bf5b3be14..dd3fe7af9 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v33"); + expect(error?.stderr).toContain("scr_runtime_abi_v35"); }); }); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index b6e489c19..16a1b1601 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -873,6 +873,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); } if (emitter.mapOpsConsts.size > 0) out.push(``); + for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`); + if (emitter.methodIdSlots.size > 0) out.push(``); for (const line of ffiCallbacks.globals) out.push(line); if (ffiCallbacks.globals.length > 0) out.push(``); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index dd114b525..8a32c0f08 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1079,6 +1079,10 @@ export class LlEmitter { readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ readonly mapOpsConsts = new Map(); + /** Method-id slots of checked-dynamic method calls + * (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the + * runtime fills with the name's id on the first call. */ + readonly methodIdSlots = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ @@ -2739,6 +2743,17 @@ export class LlEmitter { return `@${c.sym}`; } + /** The method-id slot for a constant method name (methodIdSlots). */ + methodIdSlot(name: string): string { + let sym = this.methodIdSlots.get(name); + if (!sym) { + sym = this.contentSymbol("sc_mid_", name); + this.methodIdSlots.set(name, sym); + } + this.journal?.entry("methodIdSlot", name, sym); + return `@${sym}`; + } + needBadTag(): void { this.needsBadTag = true; } @@ -4856,6 +4871,15 @@ export class LlEmitter { if (own !== value) names.set(value as string, own); return; } + case "methodIdSlot": { + let own = this.methodIdSlots.get(key); + if (own === undefined) { + own = this.contentSymbol("sc_mid_", key); + this.methodIdSlots.set(key, own); + } + if (own !== value) names.set(value as string, own); + return; + } case "classObj": addEntry(this.classObjs, key, value as { nameSym: string }); return; diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 9af677586..434435eae 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -250,8 +250,10 @@ export function emitDynamicExpr( } // Buffer.prototype method names take their scr_bytes_dyn.c entry, which // runs them on Buffer receivers and dispatches any other receiver. - const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared"; - host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); + // Each call passes its method name's id slot, so only the first call + // looks the name up. + const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -260,7 +262,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 45c18b803..7516673cd 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { /** dynInvoke's method read on an ordinary object through the site's * cache, else scr_dyn_prepare_method. Returns the owned method (or NULL, - * which scr_dyn_invoke_prepared answers with the full dispatch). */ + * which scr_dyn_invoke_prepared_id answers with the full dispatch). */ export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string { const B = host.B; const method = e.method; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 7d3dcbc02..11647ab0c 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 33 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const; +export const RUNTIME_ABI_VERSION = 35 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v35" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 532051b88..cb3f8a5d9 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 18b04b9a6..17ac1db9e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0fd5249d3..ce1041c99 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v35"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v33"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v35"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e17312713..4802a7970 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 60ef9dc49..329aac00d 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index 280aa423c..c492d43b1 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,8 +509,9 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -518,40 +519,45 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -584,5 +590,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f8dd9a0c0..f80c314b8 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v33(void) {} +void scr_runtime_abi_v35(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index d716f0de3..5633977b8 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v33(void); +void scr_runtime_abi_v35(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index cf50194c8..e0b33f539 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1296,15 +1296,19 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), - * one entry per method family: a Buffer receiver runs the method, any other - * takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for a call site whose method name is a constant: + * *mid_slot, the site's -1-initialized slot, caches the name's method id + * after the first call, so later calls skip the name lookup. */ +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared_id for Buffer.prototype method names + * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs + * the method, any other takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 14d11dc67..c536d08b1 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -8,6 +8,7 @@ * scr_json.c; this unit uses only its public surface plus ScrJsonBuf. */ #include "scr_runtime.h" +#include "scr_dyn_methods.h" #include #include @@ -549,7 +550,11 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return scr_dyn_retain(scr_dyn_undefined()); } -static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; } +/* The dispatch below maps a call's method name to an id once + * (dyn_method_of, scr_dyn_methods.h) and its branches compare ids. Names + * compare by content, and any name the branches do not test is DYN_M_NONE. + * scripts/gen-dyn-methods.mjs writes the header from the DYN_M_ ids used + * here. */ /* Handle receivers: hand the whole call to the tag's ops (installed by * the owning unit at main() — a missing install is an internal error the @@ -692,29 +697,34 @@ static bool dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) { /* Names each prototype declares BEYOND what's implemented here — these * throw the "not supported yet" Error instead of mis-answering "is not a * function". */ -static bool dyn_arr_proto_unimpl(const char *m) { - static const char *names[] = { "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_unimpl(int m) { + return m == DYN_M_toString || m == DYN_M_toLocaleString; } -static bool dyn_arr_proto_mutates(const char *m) { - static const char *names[] = { - "push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL - }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_mutates(int m) { + switch (m) { + case DYN_M_push: case DYN_M_pop: case DYN_M_shift: case DYN_M_unshift: case DYN_M_reverse: + case DYN_M_sort: case DYN_M_fill: case DYN_M_copyWithin: case DYN_M_splice: + return true; + default: + return false; + } } -static bool dyn_bytes_proto_real(const char *m) { - static const char *names[] = { "slice", "at", "indexOf", "lastIndexOf", "includes", "join", - "forEach", "map", "filter", "some", "every", "find", "findIndex", "reverse", "fill", "set", - "subarray", "sort", "keys", "values", "entries", "reduce", "reduceRight", "copyWithin", - "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_bytes_proto_real(int m) { + switch (m) { + case DYN_M_slice: case DYN_M_at: case DYN_M_indexOf: case DYN_M_lastIndexOf: case DYN_M_includes: + case DYN_M_join: case DYN_M_forEach: case DYN_M_map: case DYN_M_filter: case DYN_M_some: + case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: + case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: + case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: + case DYN_M_toString: case DYN_M_toLocaleString: + return true; + default: + return false; + } } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); /* Live native arrays: the constant-time methods that loops call once per @@ -724,18 +734,18 @@ static ScrDyn *scr_dyn_invoke_impl( * array. Anything that could observe more — a hole, argument coercion, a * multi-value push, a value the element type rejects — sets *handled false * and takes the snapshot path. */ -static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args, +static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, const char *what, bool *handled) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(recv); if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; - if (dyn_name_is(method, "push") && argc == 1 && ops->set) { + if ((mid == DYN_M_push) && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) { *handled = true; return scr_dyn_retain(scr_dyn_undefined()); @@ -746,7 +756,7 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *c *handled = true; return last; } - if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { + if ((mid == DYN_M_at) && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { double index = dyn_index_arg(args, argc, 0, 0, what); if (index < 0) index += (double)len; if (index < 0 || index >= (double)len) { @@ -786,11 +796,25 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - return scr_dyn_invoke_impl(recv, method, args, argc, what, NULL); + return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); +} + +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { + if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + /* The slot is the call site's, shared by every thread: each writes the + * same id. */ + int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); + if (mid < 0) { + mid = dyn_method_of(method); + __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); + } + return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; @@ -811,7 +835,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_HANDLE) { return scr_dynh_dispatch(recv, method, args, argc, what); } - if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(method)) { + if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(mid)) { scr_dyn_array_prototype_element_fence(recv); return NULL; } @@ -836,7 +860,7 @@ static ScrDyn *scr_dyn_invoke_impl( * snapshot identity back to the externally visible capsule. */ if (recv->kind == SCR_DYN_TYPED_REF) { bool handled = false; - ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); + ScrDyn *fast = dyn_live_array_method(recv, mid, args, argc, what, &handled); if (handled) return fast; /* Iterator helpers on a compiled generator. */ if (scr_dyn_generator_helper_hook && @@ -861,9 +885,9 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } bool mutates = materialized->kind == SCR_DYN_ARR && - dyn_arr_proto_mutates(method); + dyn_arr_proto_mutates(mid); ScrDyn *result = scr_dyn_invoke_impl( - materialized, method, args, argc, what, recv); + materialized, method, mid, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); if (result == materialized) { scr_dyn_release(result); @@ -892,18 +916,18 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_FUNC) { - if (dyn_name_is(method, "apply")) { + if ((mid == DYN_M_apply)) { return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined(), what); } - if (dyn_name_is(method, "call")) { + if ((mid == DYN_M_call)) { scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined()); ScrDyn *r = scr_dyn_call(recv, argc > 1 ? args + 1 : NULL, argc > 1 ? argc - 1 : 0, what); scr_dyn_this_pop(); return r; } - if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc); - if (dyn_name_is(method, "toString")) { + if ((mid == DYN_M_bind)) return scr_dyn_bind(recv, args, argc); + if ((mid == DYN_M_toString)) { ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method)); if (member && member->kind != SCR_DYN_UNDEF) { scr_dyn_this_push_dyn(recv); @@ -938,9 +962,9 @@ static ScrDyn *scr_dyn_invoke_impl( } if ((recv->kind == SCR_DYN_SYMBOL || recv->kind == SCR_DYN_BIGINT || recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) && - dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + (mid == DYN_M_valueOf)) return scr_dyn_retain(recv); - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toFixed")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toFixed)) { double digits = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; ScrStr *text = scr_num_to_fixed(recv->v.num, digits); @@ -949,7 +973,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(text); return result; } - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toExponential")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toExponential)) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { dyn_throw_unsupported("Number", method); return NULL; @@ -962,7 +986,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_STR) { ScrStr *s = recv->v.str; - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double start = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double end = dyn_index_arg(args, argc, 1, scr_str_utf16_len(s), what); @@ -972,16 +996,16 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) && + if ((mid == DYN_M_at) || (mid == DYN_M_concat) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); + double position = dyn_index_arg(args, argc, 1, (mid == DYN_M_lastIndexOf) ? INFINITY : 0, what); if (scr_exc_pending()) return NULL; - if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); - if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + if ((mid == DYN_M_includes)) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if ((mid == DYN_M_indexOf)) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); @@ -992,19 +1016,19 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_ARR) { - if (dyn_name_is(method, "keys") || dyn_name_is(method, "values") || dyn_name_is(method, "entries")) + if ((mid == DYN_M_keys) || (mid == DYN_M_values) || (mid == DYN_M_entries)) return scr_dyn_native_handle_iterator(callback_recv ? callback_recv : recv, - dyn_name_is(method, "keys") ? 1 : dyn_name_is(method, "entries") ? 2 : 0); + (mid == DYN_M_keys) ? 1 : (mid == DYN_M_entries) ? 2 : 0); size_t len = recv->v.arr.len; if (recv->v.arr.presence) { bool sparse = false; for (size_t i = 0; i < len; i++) if (!recv->v.arr.presence[i]) { sparse = true; break; } if (!sparse) { free(recv->v.arr.presence); recv->v.arr.presence = NULL; } - else if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || dyn_name_is(method, "filter") || - dyn_name_is(method, "some") || dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex") || dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex") || - dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight") || dyn_name_is(method, "slice") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { + else if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_reduce) || (mid == DYN_M_reduceRight) || (mid == DYN_M_slice) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes)) { ScrDyn *pack = scr_dyn_new_arr(); for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(pack, scr_dyn_retain(args[i])); ScrStr *name = scr_str_new(method, strlen(method)); @@ -1012,7 +1036,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(name); scr_dyn_release(pack); return result; - } else if (dyn_arr_proto_mutates(method) && !dyn_name_is(method, "fill") && !dyn_name_is(method, "push") && !dyn_name_is(method, "pop") && !dyn_name_is(method, "shift")) { + } else if (dyn_arr_proto_mutates(mid) && !(mid == DYN_M_fill) && !(mid == DYN_M_push) && !(mid == DYN_M_pop) && !(mid == DYN_M_shift)) { static const char message[] = "This checked array mutation over holes is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -1020,11 +1044,11 @@ static ScrDyn *scr_dyn_invoke_impl( } size_t splice_start = 0; size_t splice_removed = 0; - if (dyn_arr_proto_mutates(method)) { - bool grows = dyn_name_is(method, "push") && argc > 0; - grows = grows || (dyn_name_is(method, "unshift") && argc > 0); - bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0; - if (dyn_name_is(method, "splice")) { + if (dyn_arr_proto_mutates(mid)) { + bool grows = (mid == DYN_M_push) && argc > 0; + grows = grows || ((mid == DYN_M_unshift) && argc > 0); + bool shrinks = ((mid == DYN_M_pop) || (mid == DYN_M_shift)) && len > 0; + if ((mid == DYN_M_splice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; splice_start = dyn_rel_index(startD, len); @@ -1061,33 +1085,33 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && (dyn_name_is(method, "push") || - dyn_name_is(method, "pop") || dyn_name_is(method, "shift") || - dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) { + if (recv->v.arr.frozen && ((mid == DYN_M_push) || + (mid == DYN_M_pop) || (mid == DYN_M_shift) || + (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { static const char message[] = "Cannot assign to read only property 'length'"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) || - (recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) || - (dyn_name_is(method, "reverse") && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || + (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || + ((mid == DYN_M_reverse) && len > 1)))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } } - if (dyn_name_is(method, "push")) { + if ((mid == DYN_M_push)) { for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *result = scr_dyn_arr_at(recv, (double)(len - 1)); if (!result) return NULL; scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); return result; } - if (dyn_name_is(method, "shift")) { + if ((mid == DYN_M_shift)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); if (recv->v.arr.sealed) { if (recv->v.arr.frozen) { @@ -1109,7 +1133,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = len - 1; return first; /* ownership moves out */ } - if (dyn_name_is(method, "unshift")) { + if ((mid == DYN_M_unshift)) { /* Grow the backing store before shifting the old elements. The * appended placeholders are immortal, so overwriting them does not * abandon retained argument references. */ @@ -1121,7 +1145,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "splice")) { + if ((mid == DYN_M_splice)) { size_t start = splice_start; size_t removed = splice_removed; size_t inserted = argc > 2 ? argc - 2 : 0; @@ -1151,7 +1175,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = next_len; return out; } - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 1, (double)len, what); @@ -1163,7 +1187,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start; i < end; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i])); return out; } - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1171,17 +1195,17 @@ static ScrDyn *scr_dyn_invoke_impl( if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_arr_at(recv, idx); } - if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + if ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (len == 0) return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); - double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0; + double from = (mid == DYN_M_lastIndexOf) ? (double)len - 1 : 0; if (argc > 1) { from = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; } if (!dyn_arr_length_unchanged(recv, len)) return NULL; - if (dyn_name_is(method, "lastIndexOf")) { + if ((mid == DYN_M_lastIndexOf)) { double first = from < 0 ? (double)len + from : from; if (first >= (double)len) first = (double)len - 1; for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) { @@ -1193,16 +1217,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (first < 0) first = 0; for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) { const ScrDyn *item = recv->v.arr.items[i]; - bool nan_match = dyn_name_is(method, "includes") && + bool nan_match = (mid == DYN_M_includes) && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM && item->v.num != item->v.num && needle->v.num != needle->v.num; if (nan_match || scr_dyn_strict_eq(item, needle)) { - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); } } - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t i = len; i > 0; i--) { @@ -1215,16 +1239,16 @@ static ScrDyn *scr_dyn_invoke_impl( bool matched = scr_dyn_truthy(result); scr_dyn_release(result); if (matched) { - if (dyn_name_is(method, "findLast")) return item; + if ((mid == DYN_M_findLast)) return item; scr_dyn_release(item); return scr_dyn_new_num((double)index); } scr_dyn_release(item); } - return dyn_name_is(method, "findLast") + return (mid == DYN_M_findLast) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "join")) { + if ((mid == DYN_M_join)) { ScrJsonBuf b; scr_jb_init(&b); ScrStr *separator = argc > 0 && args[0]->kind != SCR_DYN_UNDEF @@ -1250,7 +1274,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return r; } - if (dyn_name_is(method, "concat")) { + if ((mid == DYN_M_concat)) { ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { if (!dyn_arr_copy_item(out, recv, i, true)) { scr_dyn_release(out); return NULL; } @@ -1271,7 +1295,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "flat")) { + if ((mid == DYN_M_flat)) { double depth = dyn_index_arg(args, argc, 0, 1, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1283,7 +1307,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "reverse")) { + if ((mid == DYN_M_reverse)) { for (size_t i = 0; i < len / 2; i++) { ScrDyn *tmp = recv->v.arr.items[i]; recv->v.arr.items[i] = recv->v.arr.items[len - 1 - i]; @@ -1291,12 +1315,12 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toReversed")) { + if ((mid == DYN_M_toReversed)) { ScrDyn *out = scr_dyn_new_arr(); if (!dyn_arr_reversed_copy(out, recv)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "with")) { + if ((mid == DYN_M_with)) { double indexD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1314,7 +1338,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "toSpliced")) { + if ((mid == DYN_M_toSpliced)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; size_t start = dyn_rel_index(startD, len); @@ -1331,7 +1355,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start + delete_count; i < len; i++) if (!dyn_arr_copy_item(out, recv, i, false)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "fill")) { + if ((mid == DYN_M_fill)) { double startD = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 2, (double)len, what); @@ -1359,7 +1383,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double targetD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double startD = dyn_index_arg(args, argc, 1, 0, what); @@ -1392,13 +1416,13 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || - dyn_name_is(method, "filter") || dyn_name_is(method, "some") || - dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex")) { + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0]; - ScrDyn *out = (dyn_name_is(method, "map") || dyn_name_is(method, "filter")) ? scr_dyn_new_arr() : NULL; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; /* Array iteration methods snapshot length before the first callback. * A shrinking callback can remove a later dense entry; an expanding * callback must not make the new tail observable to this pass. Live @@ -1413,30 +1437,30 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if (dyn_name_is(method, "map")) { + if ((mid == DYN_M_map)) { scr_dyn_arr_push(out, r); /* ownership moves in */ r = NULL; } else { bool truthy = scr_dyn_truthy(r); scr_dyn_release(r); - if (dyn_name_is(method, "filter") && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if (dyn_name_is(method, "some") && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if (dyn_name_is(method, "every") && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if (dyn_name_is(method, "find") && truthy) return item; /* +1 moves out */ - if (dyn_name_is(method, "findIndex") && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } } scr_dyn_release(item); } if (out) return out; - if (dyn_name_is(method, "some")) return scr_dyn_new_bool(false); - if (dyn_name_is(method, "every")) return scr_dyn_new_bool(true); - if (dyn_name_is(method, "find")) return scr_dyn_retain(scr_dyn_undefined()); - if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1); + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ } - if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) { + if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; - bool backward = dyn_name_is(method, "reduceRight"); + bool backward = (mid == DYN_M_reduceRight); ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t step = 0; step < len; step++) { @@ -1458,7 +1482,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if (dyn_name_is(method, "flatMap")) { + if ((mid == DYN_M_flatMap)) { /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 * flatten. Native dyn-array results flatten element-by-element; a * WRAPPED engine array flattens through the routed keyed reads @@ -1496,7 +1520,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "sort")) { + if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { /* V8 appends the received value's string image. */ @@ -1510,7 +1534,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL; return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toSorted")) { + if ((mid == DYN_M_toSorted)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { static const char message[] = "The comparison function must be either a function or undefined"; @@ -1525,7 +1549,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_arr_proto_unimpl(method)) { + if (dyn_arr_proto_unimpl(mid)) { dyn_throw_unsupported("Array", method); return NULL; } @@ -1541,14 +1565,14 @@ static ScrDyn *scr_dyn_invoke_impl( #ifdef SCR_LIB scr_trap("scriptc: promise dispatch is unavailable in library mode\n"); #else - if (dyn_name_is(method, "then")) { + if ((mid == DYN_M_then)) { return scr_dyn_promise_then(recv->v.promise, argc >= 1 ? args[0] : NULL, argc >= 2 ? args[1] : NULL, NULL); } - if (dyn_name_is(method, "catch")) { + if ((mid == DYN_M_catch)) { return scr_dyn_promise_then(recv->v.promise, NULL, argc >= 1 ? args[0] : NULL, NULL); } - if (dyn_name_is(method, "finally")) { + if ((mid == DYN_M_finally)) { return scr_dyn_promise_then(recv->v.promise, NULL, NULL, argc >= 1 ? args[0] : NULL); } dyn_throw_not_fn(what); @@ -1600,44 +1624,44 @@ static ScrDyn *scr_dyn_invoke_impl( } /* The typed array methods below are not DataView's; it inherits * Object.prototype's. */ - if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { + if ((mid == DYN_M_toString) || (mid == DYN_M_toLocaleString)) { ScrStr *tag = scr_str_new("[object DataView]", 17); ScrDyn *out = scr_dyn_new_str(tag); scr_str_release(tag); return out; } - if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + if ((mid == DYN_M_valueOf)) return scr_dyn_retain(recv); dyn_throw_not_fn(what); return NULL; } size_t blen = bytes->len; - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double idx = iD < 0 ? (double)blen + iD : iD; if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_new_num(scr_bytes_get(bytes, idx)); } - if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { + if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { /* subarray and Buffer.slice alias their source. TypedArray.slice * owns an independent copy; all keep the receiver's Buffer flavor. */ double startD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") + ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - if (dyn_name_is(method, "set")) { + if ((mid == DYN_M_set)) { double offset = dyn_bytes_index_arg(args, argc, 1, 0); if (scr_exc_pending()) return NULL; scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double target = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1647,7 +1671,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "fill") && !recv->buffer) { + if ((mid == DYN_M_fill) && !recv->buffer) { double value; if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1657,8 +1681,8 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) { - ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF + if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { + ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); if (scr_exc_pending()) { scr_str_release(separator); return NULL; } ScrStr *joined = scr_bytes_join(bytes, separator); @@ -1667,7 +1691,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } - if (dyn_bytes_proto_real(method)) { + if (dyn_bytes_proto_real(mid)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; } diff --git a/packages/runtime/src/scr_dyn_methods.h b/packages/runtime/src/scr_dyn_methods.h new file mode 100644 index 000000000..49e5c7254 --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.h @@ -0,0 +1,160 @@ +/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c + * uses. Do not edit by hand. */ +#ifndef SCR_DYN_METHODS_H +#define SCR_DYN_METHODS_H + +#include +#include + +enum { + DYN_M_NONE, + DYN_M_apply, + DYN_M_at, + DYN_M_bind, + DYN_M_call, + DYN_M_catch, + DYN_M_concat, + DYN_M_copyWithin, + DYN_M_entries, + DYN_M_every, + DYN_M_fill, + DYN_M_filter, + DYN_M_finally, + DYN_M_find, + DYN_M_findIndex, + DYN_M_findLast, + DYN_M_findLastIndex, + DYN_M_flat, + DYN_M_flatMap, + DYN_M_forEach, + DYN_M_includes, + DYN_M_indexOf, + DYN_M_join, + DYN_M_keys, + DYN_M_lastIndexOf, + DYN_M_map, + DYN_M_pop, + DYN_M_push, + DYN_M_reduce, + DYN_M_reduceRight, + DYN_M_reverse, + DYN_M_set, + DYN_M_shift, + DYN_M_slice, + DYN_M_some, + DYN_M_sort, + DYN_M_splice, + DYN_M_subarray, + DYN_M_then, + DYN_M_toExponential, + DYN_M_toFixed, + DYN_M_toLocaleString, + DYN_M_toReversed, + DYN_M_toSorted, + DYN_M_toSpliced, + DYN_M_toString, + DYN_M_unshift, + DYN_M_valueOf, + DYN_M_values, + DYN_M_with, + DYN_M_COUNT +}; + +static const char *const dyn_method_names[DYN_M_COUNT] = { + NULL, + "apply", + "at", + "bind", + "call", + "catch", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "finally", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "set", + "shift", + "slice", + "some", + "sort", + "splice", + "subarray", + "then", + "toExponential", + "toFixed", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "valueOf", + "values", + "with", +}; + +static const unsigned char dyn_method_lengths[DYN_M_COUNT] = { + 0, 5, 2, 4, 4, 5, 6, 10, 7, 5, 4, 6, 7, 4, 9, 8, + 13, 4, 7, 7, 8, 7, 4, 4, 11, 3, 3, 4, 6, 11, 7, 3, + 5, 5, 4, 4, 6, 8, 4, 13, 7, 14, 10, 8, 9, 8, 7, 7, + 6, 4, +}; + +/* Each name's id at its slot (or the next free one); 0 ends a probe. */ +static const unsigned char dyn_method_slots[256] = { + 0, 0, 0, 0, 12, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 14, 0, 0, 27, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 28, 3, 0, + 22, 0, 20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, + 0, 0, 0, 0, 0, 33, 31, 0, 0, 0, 0, 0, 36, 35, 0, 0, + 0, 0, 0, 0, 23, 4, 32, 0, 5, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 6, 0, 0, 37, 0, + 0, 40, 0, 0, 0, 0, 0, 0, 43, 0, 0, 45, 0, 0, 0, 44, + 0, 7, 0, 0, 0, 0, 42, 0, 25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, + 46, 0, 0, 39, 0, 41, 0, 9, 0, 0, 13, 0, 0, 0, 0, 8, + 0, 47, 10, 0, 0, 0, 0, 48, 0, 0, 17, 0, 0, 49, 0, 0, + 0, 0, 0, 19, 0, 0, 11, 0, 0, 0, 0, 18, 0, 0, 0, 0, +}; + +#define DYN_METHOD_MAX_LENGTH 14 + +/* The id of a method name, compared by content; DYN_M_NONE for any other + * name. */ +static inline int dyn_method_of(const char *m) { + size_t length = strlen(m); + if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE; + unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 + + (unsigned char)m[length - 1]) & 255; + for (;;) { + unsigned id = dyn_method_slots[slot]; + if (id == 0) return DYN_M_NONE; + if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id; + slot = (slot + 1) & 255; + } +} + +#endif diff --git a/packages/runtime/src/scr_dyn_methods.test.ts b/packages/runtime/src/scr_dyn_methods.test.ts new file mode 100644 index 000000000..77911aa2d --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.test.ts @@ -0,0 +1,10 @@ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; +import { expect, test } from "vitest"; + +test("scr_dyn_methods.h lists every method id the checked-dynamic dispatch compares", () => { + const script = join(import.meta.dirname, "../../../scripts/gen-dyn-methods.mjs"); + expect(() => + execFileSync(process.execPath, [script, "--check"], { stdio: "pipe" }), + ).not.toThrow(); +}); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 3d10183f4..a23395c91 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -354,17 +354,23 @@ static bool scr_worker_transfer_has(const ScrDyn *transfer, const ScrWorkerView return false; } +/* Whether a method name is `name`: first characters compare inline, so + * the dispatch ladders below call strcmp only for names that can match. */ +static inline bool method_is(const char *method, const char *name) { + return method[0] == name[0] && strcmp(method, name) == 0; +} + static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "hasRef")) + if (view->port && method_is(method, "hasRef")) return scr_dyn_new_bool(view->referenced && (!view->closed || (view->channel && view->closing))); - if (!view->port && !strcmp(method, "terminate")) { + if (!view->port && method_is(method, "terminate")) { if (!view->termination) { view->termination = scr_promise_new(); if (view->closed) scr_promise_fulfill_ref(view->termination, @@ -373,7 +379,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_new_promise(view->termination); } - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { /* Worker drops messages after exit before validating or cloning. A * closed MessagePort still validates its payload, as Node does. */ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined()); @@ -405,9 +411,9 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho else scr_worker_thread_post(view->thread, message); return scr_dyn_retain(scr_dyn_undefined()); } - bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener"); - bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener"); - if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) { + bool once = method_is(method, "once") || method_is(method, "prependOnceListener"); + bool prepend = method_is(method, "prependListener") || method_is(method, "prependOnceListener"); + if (once || prepend || method_is(method, "on") || method_is(method, "addListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -416,7 +422,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { + if (method_is(method, "off") || method_is(method, "removeListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -425,21 +431,21 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "removeAllListeners")) { + if (method_is(method, "removeAllListeners")) { bool all = !argc || args[0]->kind == SCR_DYN_UNDEF; if (!all && args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "listenerCount")) { + if (method_is(method, "listenerCount")) { if (!argc || args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1]) : scr_emitter_listener_count(view->events, args[0]->v.str)); } - if (view->port && !strcmp(method, "close")) { + if (view->port && method_is(method, "close")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { scr_dyn_check_listener(args[0], "callback"); if (scr_exc_pending()) return NULL; @@ -460,7 +466,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "start")) { + if (view->port && method_is(method, "start")) { view->started = true; return scr_dyn_retain(scr_dyn_undefined()); } @@ -1021,7 +1027,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { if (view->closed) { scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); return NULL; @@ -1045,7 +1051,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, scr_message_free(first); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "close")) { + if (method_is(method, "close")) { if (!view->closed) { view->closed = true; scr_broadcast_leave(view->channel); @@ -1053,8 +1059,8 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(self); } ScrDyn *out = NULL; diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs new file mode 100644 index 000000000..fe4f81a90 --- /dev/null +++ b/scripts/gen-dyn-methods.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the + * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the + * names, and an open-addressed hash table mapping a name to its id. The ids + * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the + * dispatch code. + * + * node scripts/gen-dyn-methods.mjs write the header + * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ +import { readFileSync, writeFileSync } from "node:fs"; + +const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); +const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); +const SLOTS = 256; + +const used = new Set(); +for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) + if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); +const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); + +// The slot dyn_method_of in the header computes. +const slotOf = (name) => + (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); +const slots = new Array(SLOTS).fill(0); +names.forEach((name, i) => { + let slot = slotOf(name); + while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); + slots[slot] = i + 1; +}); + +const rows = (values, width) => { + const out = []; + for (let i = 0; i < values.length; i += width) + out.push(` ${values.slice(i, i + width).join(", ")},`); + return out; +}; +const text = [ + "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", + " * uses. Do not edit by hand. */", + "#ifndef SCR_DYN_METHODS_H", + "#define SCR_DYN_METHODS_H", + "", + "#include ", + "#include ", + "", + "enum {", + " DYN_M_NONE,", + ...names.map((name) => ` DYN_M_${name},`), + " DYN_M_COUNT", + "};", + "", + "static const char *const dyn_method_names[DYN_M_COUNT] = {", + " NULL,", + ...names.map((name) => ` ${JSON.stringify(name)},`), + "};", + "", + "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", + ...rows([0, ...names.map((name) => name.length)], 16), + "};", + "", + `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, + `static const unsigned char dyn_method_slots[${SLOTS}] = {`, + ...rows(slots, 16), + "};", + "", + `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, + "", + "/* The id of a method name, compared by content; DYN_M_NONE for any other", + " * name. */", + "static inline int dyn_method_of(const char *m) {", + " size_t length = strlen(m);", + " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", + " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", + ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, + " for (;;) {", + " unsigned id = dyn_method_slots[slot];", + " if (id == 0) return DYN_M_NONE;", + " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", + ` slot = (slot + 1) & ${SLOTS - 1};`, + " }", + "}", + "", + "#endif", + "", +].join("\n"); + +if (process.argv.includes("--check")) { + let current = ""; + try { + current = readFileSync(target, "utf8"); + } catch {} + if (current !== text) { + console.error( + "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", + ); + process.exit(1); + } +} else { + writeFileSync(target, text); +} diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 3158de997..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,7 +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. 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. */ @@ -408,6 +409,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", + "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 4d5546ab6..85fa73410 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 4a17b3f461b5223eca5e29897164d5a287991f4b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:05:57 -0500 Subject: [PATCH 126/169] Keep the dynamic call for observed methods called with a spread argument --- .../src/frontend/lowering/lower-calls.ts | 5 ++- .../test/observed-method-guard.test.ts | 42 +++++++++++++++++++ .../main.ts | 28 +++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 493d979f5..fd26a832b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10198,10 +10198,13 @@ export function lowerObjectMethodCall( // The declaring class's original value, built with the observed slot. // An override below (a virtual call) keeps the dynamic call: a slot // reassigned to an ancestor's method must not reach the override. So do - // methods declared in unchecked JavaScript, whose results are inferred. + // methods declared in unchecked JavaScript, whose results are inferred, + // and calls with a spread argument: the dynamic call drains the spread + // through the iterator's current `next`, which may be a replacement. const declarerFile = found.declarer.decl ? ts.getSourceFileOfNode(found.declarer.decl) : null; const original = virtual || + call.arguments.nodes.some(ts.isSpreadElement) || !declarerFile || (isJsSourceFile(declarerFile) && !lowerer.checkedJsFile(declarerFile)) ? undefined diff --git a/packages/compiler/test/observed-method-guard.test.ts b/packages/compiler/test/observed-method-guard.test.ts index 5964c482b..cafe872f9 100644 --- a/packages/compiler/test/observed-method-guard.test.ts +++ b/packages/compiler/test/observed-method-guard.test.ts @@ -75,3 +75,45 @@ test("typed calls of an observed method call it natively while its slot is uncha true, ); }); + +// A spread argument keeps the dynamic call, which drains the spread through +// the iterator's current `next` (a replacement included). +test("typed calls of an observed method with a spread argument keep the dynamic call", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-")); + dirs.push(dir); + const entry = join(dir, "main.ts"); + const outDir = join(dir, ".scriptc"); + const outPath = join(outDir, "main.ir.json"); + await writeFile( + join(dir, "untyped.js"), + "export function put(target, key, value) { target[key] = value; }\n", + ); + await writeFile( + entry, + [ + 'import { put } from "./untyped.js";', + "class Bag {", + " sum(...values: number[]): number { let s = 0; for (const n of values) s += n; return s; }", + "}", + "const bag = new Bag();", + "put({}, String(Math.random()), 1);", + "export function spread(b: Bag, values: number[]): number { return b.sum(...values); }", + "console.log(spread(bag, [1, 2]));", + "", + ].join("\n"), + ); + const result = await compile(entry, { outDir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const module = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(module)).toEqual([]); + const spread = module.functions.find((fn) => fn.name.endsWith("spread")); + expect(spread).toBeDefined(); + const guards = recordsOf(spread).filter( + (record) => + record.kind === "ternary" && + (record.cond as IrRecord | undefined)?.kind === "libCall" && + (record.cond as IrRecord).fn === "dyn.sameValue", + ); + expect(guards).toHaveLength(0); +}); diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts index f7449f1af..c5475b5d9 100644 --- a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts @@ -57,3 +57,31 @@ patch(proto, "list", () => ["patched"]); console.log(repo.list().join(",")); patch(proto, "list", saved); console.log(repo.list().join(",")); + +// a spread argument drains its iterator through the iterator's current `next` +class Bag { + sum(...values: number[]): number { + let total = 0; + for (const value of values) total += value; + return total; + } +} +class Countdown { + private left: number; + constructor(left: number) { + this.left = left; + } + next(): IteratorResult { + return this.left > 0 ? { done: false, value: this.left-- } : { done: true, value: undefined }; + } + [Symbol.iterator](): Countdown { + return this; + } +} +const bag = new Bag(); +console.log(bag.sum(...[1, 2, 3]), bag.sum(...new Countdown(3))); +const replaced = new Countdown(3); +patch(replaced, "next", function (): IteratorResult { + return { done: true, value: undefined }; +}); +console.log(bag.sum(...replaced)); From 257d4dc5cb9f96e6eb097c342d0acd99942cfb28 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:11:30 -0500 Subject: [PATCH 127/169] Declare flock on macOS in the native compiler host host.c defines _POSIX_C_SOURCE, under which Darwin's hides flock and LOCK_EX/LOCK_UN, so the native compiler build failed on macOS with an implicit declaration error. Define _DARWIN_C_SOURCE on Apple targets before the system headers. Linux preprocessing is unchanged. --- packages/compiler/native/host.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index eeead7eff..f54bdaa3f 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -1,6 +1,10 @@ #ifndef _WIN32 #define _POSIX_C_SOURCE 200809L #endif +/* flock and LOCK_EX/LOCK_UN, which Darwin hides under _POSIX_C_SOURCE. */ +#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) +#define _DARWIN_C_SOURCE 1 +#endif #include #include #include From cba3fa6a05ff6c2a00741671997730605aaf45bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:25:59 -0500 Subject: [PATCH 128/169] Keep projected files valid for bare async arrows and unbraced bodies, and adopt tuple elements - A bare arrow parameter after `async` (`async x => ...`) gets parentheses with its projected type, as `x => ...` does. Without them the rewritten file failed its check and lost its projection (SC6102). - A cast that starts an unbraced loop or `if` body gets no `;` guard. Only a statement in a statement list can continue the previous line, and in a body the `;` became the body, which moved the statement after the loop. - Adoption walks a tuple's elements through the live element capsules of the tuple's view, so references package code holds to them see the native values. - scr_runtime.h declares scr_dyn_decl_adopt (in scr_dyn.h), which the backend emits, so llvm-runtime-abi.test.ts finds its prototype. - npm.test.ts gives zod3-adoption and zod3-boundary the long per-case timeout of the other zod 3 cases; their sanitized builds compile zod's types module too. --- .../compiler/src/frontend/decl-projection.ts | 20 +++++++++-- packages/runtime/src/scr_decl_adopt.c | 27 ++++++++++++++- packages/runtime/src/scr_dyn.h | 6 ++++ .../fixtures/npm/cases/zod3-adoption/main.ts | 9 +++++ .../npm/cases/zod3-boundary/rewrites.ts | 21 ++++++++++++ tests/harness/decl-boundary.test.ts | 33 ++++++++++++++++++- tests/harness/npm.test.ts | 9 +++-- 7 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/npm/cases/zod3-boundary/rewrites.ts diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 4ff843c8b..31d32834e 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -605,11 +605,15 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const name = jsNode.name; const nameStart = ts.tokenStartOfNode(name); const text = jsSf.text; + // `x => ...` or `async x => ...`: nothing but the modifiers comes + // before the parameter, which then needs parentheses for its type. + const head = ts.isArrowFunction(fn) + ? ((ts.getModifiers(fn) ?? []).at(-1)?.end ?? ts.tokenStartOfNode(fn)) + : nameStart; const bareArrow = ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && - text[ts.tokenStartOfNode(fn)] !== "(" && - text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === ""; + text.slice(head, nameStart).trim() === ""; const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; if (bareArrow) { fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); @@ -727,10 +731,20 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // semicolon continues the previous expression: guard it. let up: ts.Node | undefined = e; while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; + // Only a statement in a statement list can follow such a line; an + // unbraced body (`for (...) S.parse(x).run()`) follows its header, + // and a `;` there would become the body. + const list = up?.parent?.parent; if ( up?.parent !== undefined && ts.isExpressionStatement(up.parent) && - ts.tokenStartOfNode(up.parent) === start + ts.tokenStartOfNode(up.parent) === start && + list !== undefined && + (ts.isBlock(list) || + ts.isSourceFile(list) || + ts.isModuleBlock(list) || + ts.isCaseClause(list) || + ts.isDefaultClause(list)) ) prefix = ";("; void stmt; diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c index 3173ffb84..7372826ab 100644 --- a/packages/runtime/src/scr_decl_adopt.c +++ b/packages/runtime/src/scr_decl_adopt.c @@ -22,7 +22,6 @@ #include void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); typedef struct { ScrDyn *node; @@ -120,6 +119,32 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu return false; } } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && !capsule->v.typed_ref.array && + n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + /* A capsule without element hooks over an array: a tuple, whose index + * field names get no record hooks. Its view holds a live capsule of + * each record or array element, which the source element adopts (the + * walk skips the other elements). */ + ScrDyn *view = capsule->v.typed_ref.materialize(ptr); + if (scr_exc_pending()) { + scr_dyn_release(view); + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + if (view && view->kind == SCR_DYN_ARR) { + size_t len = n->v.arr.len < view->v.arr.len ? n->v.arr.len : view->v.arr.len; + for (size_t i = 0; i < len; i++) { + ScrDyn *child = n->v.arr.items[i], *element = view->v.arr.items[i]; + if (!child || !element || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, scr_dyn_retain(element), &p, depth + 1)) { + scr_dyn_release(view); + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + scr_dyn_release(view); } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index cf50194c8..e18719ec1 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -368,6 +368,12 @@ typedef struct ScrDynTypedRecordOps { /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d); +/* The declaration-typed boundary's adoption (scr_decl_adopt.c, linked by + * programs with boundary casts): the nodes of `source` that something else + * still references become live capsules of the native value `capsule` + * holds. Both borrowed; a node that is not plain data throws the path + * TypeError before anything changes. */ +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); /* Prototype members of a compiled class seen as `unknown`, emitted per * class by the compiler: the string names of methods, getters and setters diff --git a/tests/fixtures/npm/cases/zod3-adoption/main.ts b/tests/fixtures/npm/cases/zod3-adoption/main.ts index 6566d0e3a..d21ff69ee 100644 --- a/tests/fixtures/npm/cases/zod3-adoption/main.ts +++ b/tests/fixtures/npm/cases/zod3-adoption/main.ts @@ -37,3 +37,12 @@ cyc.meta = cyc; const back = AsRow.parse(cyc); back.id = 10; console.log("cycle", cyc.id, back === cyc, back.meta === cyc); +// A tuple's elements keep their identity too: the package-side array +// still holds the element that typed code mutates. +const Pair = z.custom<[{ id: number; tags: string[] }, string]>(); +const pairSource = JSON.parse('[{"id":1,"tags":["a"]},"s"]') as unknown[]; +const pairHead = pairSource[0]; +const pair = Pair.parse(pairSource); +pair[0].tags.push("b"); +pair[0].id = 2; +console.log("tuple", JSON.stringify(pairHead), JSON.stringify(pairSource), pair[1]); diff --git a/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts b/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts new file mode 100644 index 000000000..7a98cccf0 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts @@ -0,0 +1,21 @@ +// Rewrites the declaration-typed boundary makes in user code: a typed +// parameter of a bare `async` arrow zod calls, and a cast at the start of +// an unbraced loop body. +import { z } from "zod"; + +const Box = z.object({ n: z.number() }); +const raw: unknown = { n: 2 }; + +// The cast opens the loop body's statement; the loop runs it each time. +let seen = 0; +for (let i = 0; i < 3; i++) Box.parse(raw).n > 0 && seen++; +let left = 2; +while (left-- > 0) Box.parse(raw).n > 1 && seen++; +console.log("loops", seen); + +// zod calls the callback with the string it checked. +const Length = z.string().transform(async s => s.length + 1); +async function main(): Promise { + console.log("async arrow", await Length.parseAsync("abc")); +} +void main(); diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts index 6729dd530..931208836 100644 --- a/tests/harness/decl-boundary.test.ts +++ b/tests/harness/decl-boundary.test.ts @@ -3,7 +3,10 @@ * naming the path, and a value the native record cannot represent (a * frozen object) fails with the reason. Node does not check, so this is a * native-only test; npm.test.ts runs the differential cases - * (zod3-boundary, zod3-adoption, zod3-infer). */ + * (zod3-boundary, zod3-adoption, zod3-infer). The rewrite test checks that + * a file keeps its projection (no SC6102 note) where the rewrite has to + * parenthesize a bare async arrow's parameter and cast the start of an + * unbraced loop body. */ import { execFile } from "node:child_process"; import { mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -35,3 +38,31 @@ test( ); }, ); + +test( + "boundary rewrites keep bare async arrows and unbraced loop bodies valid", + { timeout: 600_000 }, + async () => { + const rewrites = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/rewrites.ts"); + mkdirSync(outDir, { recursive: true }); + const result = await compile(rewrites, { + outPath: join(outDir, "rewrites"), + outDir, + sanitize, + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + // The file keeps its projection: no SC6102 note drops it (a build + // that lowered nothing carries no warnings at all). + expect((result.warnings ?? []).map((w) => w.code)).not.toContain("SC6102"); + const pending = execFileAsync(result.binaryPath, [], { + cwd: dirname(rewrites), + encoding: "utf8", + }); + pending.child.stdin?.end(); + const { stdout } = await pending; + // Node's output: every loop iteration runs the cast statement. + expect(stdout).toBe("loops 5\nasync arrow 4\n"); + }, +); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 9a13c0968..461b7fbcd 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -112,13 +112,16 @@ const KNOWN: Record = { const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); /** Per-case timeouts past the default 120 s: the undici cases compile the - * built-in undici implementation's module graph, and zod3 and zod3-infer - * compile zod 3's 3,700-line types module. A sanitized build of either - * takes minutes on a CI runner. */ + * built-in undici implementation's module graph, and the zod 3 cases + * (zod3, zod3-adoption, zod3-boundary, zod3-infer) compile zod 3's + * 3,700-line types module. A sanitized build of either takes minutes on a + * CI runner. */ const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000, zod3: 600_000, + "zod3-adoption": 600_000, + "zod3-boundary": 600_000, "zod3-infer": 600_000, }; From 108df00b295e8ce45f56cda16d6614b889987fc7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:37:49 -0500 Subject: [PATCH 129/169] Keep wrapped array literals native in typed global views A fresh array literal wrapped in parentheses or `satisfies` on the right side of a write to a member of a typed view of the global object (`g.cache ??= ([])`, `g.cache ||= [] satisfies number[]`) is now built as the declared native array too, so typed reads return the same array. globalViewSlot walks up through those wrappers to the write's right operand and answers it, and the slot type comes from that operand's contextual type. --- .../compiler/src/frontend/lowering/lowerer.ts | 37 ++++++++++++------- tests/corpus/7366-typed-global-view-arrays.ts | 16 ++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 554b703e9..4f560d807 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10607,25 +10607,31 @@ export class Lowerer { return this.coerceInto(node, value, expected); } - /** Lower for a known destination, then apply ordinary value coercion. - * Fresh arrays and records use the destination layout directly. */ - /** `value` is the right side of a write to a member of a typed view of the - * global object (`(globalThis as typeof globalThis & { k?: T }).k = value`, - * also through `??=` and `||=`). */ - private globalViewSlot(value: ts.Expression): boolean { - const write = value.parent; + /** The right side of a write to a member of a typed view of the global + * object (`(globalThis as typeof globalThis & { k?: T }).k = value`, also + * through `??=` and `||=`) that is `value` itself or wraps only it in + * parentheses or `satisfies`; null when `value` is not in such a write. */ + private globalViewSlot(value: ts.Expression): ts.Expression | null { + let operand: ts.Expression = value; + while ( + operand.parent !== undefined && + (ts.isParenthesizedExpression(operand.parent) || ts.isSatisfiesExpression(operand.parent)) + ) + operand = operand.parent; + const write = operand.parent; if ( + write === undefined || !ts.isBinaryExpression(write) || - write.right !== value || + write.right !== operand || (write.operatorToken.kind !== ts.SyntaxKind.EqualsToken && write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) ) - return false; + return null; let target = write.left; while (ts.isParenthesizedExpression(target)) target = target.expression; if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) - return false; + return null; const receiver = this.typeOf(target.expression); const isGlobal = (type: ts.Type): boolean => { const symbol = type.getSymbol(); @@ -10635,12 +10641,14 @@ export class Lowerer { symbol.declarations.length === 0 ); }; - return ( - isGlobal(receiver) || + return isGlobal(receiver) || (receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal)) - ); + ? operand + : null; } + /** Lower for a known destination, then apply ordinary value coercion. + * Fresh arrays and records use the destination layout directly. */ lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr { // These wrappers do not change the value or its inferred type. Keep a // fresh literal visible to destination-directed construction. Assertions @@ -10712,7 +10720,8 @@ export class Lowerer { // the native array and store it boxed: every typed read then extracts // that same array (a dynamic literal would be copied on each read, // losing writes such as push). - const slotType = this.globalViewSlot(x) ? this.checker.getContextualType(x) : undefined; + const slot = this.globalViewSlot(x); + const slotType = slot ? this.checker.getContextualType(slot) : undefined; const declared = slotType && this.checker.getNonNullableType(slotType); const native = declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) diff --git a/tests/corpus/7366-typed-global-view-arrays.ts b/tests/corpus/7366-typed-global-view-arrays.ts index 3b24620d5..e2d9b23ff 100644 --- a/tests/corpus/7366-typed-global-view-arrays.ts +++ b/tests/corpus/7366-typed-global-view-arrays.ts @@ -30,3 +30,19 @@ const counts = globalThis as typeof globalThis & { __exampleCounts?: number[][] counts.__exampleCounts = [[1], [2, 3]]; counts.__exampleCounts[1]!.push(4); console.log(JSON.stringify(counts.__exampleCounts)); + +// a parenthesized or `satisfies`-wrapped literal is kept the same way +type Wrapped = typeof globalThis & { __exampleWrapped?: number[]; __exampleSatisfied?: number[] }; +function wrapped(): number[] { + const g = globalThis as Wrapped; + g.__exampleWrapped ??= ([5]); + return g.__exampleWrapped; +} +function satisfied(): number[] { + const g = globalThis as Wrapped; + g.__exampleSatisfied ||= [7] satisfies number[]; + return g.__exampleSatisfied; +} +wrapped().push(6); +satisfied().push(8); +console.log(wrapped().join(","), wrapped() === wrapped(), satisfied().join(","), satisfied() === satisfied()); From 20d0a6c7eb8cfd234cdd8ddc22ade242e20fe08b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:44:07 -0700 Subject: [PATCH 130/169] Resolve a forward local class per instance in hoisted functions A JavaScript package function lowered once per argument-type tuple collects its local class per instance. A function hoisted above the class statement in a later instance resolved the class an earlier instance collected (zod 4's $constructor: "expected class constructor"). It now takes the forward-class TDZ box unless the collected class is this instance's. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/identifiers.ts | 16 ++++++++- .../main.ts.json | 6 ++++ .../7614-npm-local-class-per-instance/main.ts | 26 ++++++++++++++ .../node_modules/ctor-kit/index.d.ts | 5 +++ .../node_modules/ctor-kit/index.js | 34 +++++++++++++++++++ .../node_modules/ctor-kit/package.json | 1 + .../main.ts.json | 1 + 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json create mode 100644 tests/corpus/7614-npm-local-class-per-instance/main.ts create mode 100644 tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts create mode 100644 tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js create mode 100644 tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json create mode 100644 tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 48106949e..ef0d8a2d1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -860,7 +860,21 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // reach here — their hooks claim the property/new forms first). { const sym = lowerer.resolveValueSymbol(expr); - const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; + let classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; + // A class local to a function lowered once per instantiation + // (`$constructor` called with different argument types) registers per + // instance under the same symbol: a hoisted function of a later + // instance, lowered before its own class statement, must not reach the + // class an earlier instance collected. + const declaration = sym?.valueDeclaration; + if ( + classInfo && + lowerer.localClassInstantiations.length > 0 && + declaration && + ts.isClassDeclaration(declaration) && + lowerer.classNamer(declaration) !== classInfo.def.name + ) + classInfo = undefined; // A local class referenced from a function hoisted above its statement: // its TDZ box, holding the class as a checked-dynamic constructor. if (!classInfo && sym && predeclareForwardClass(lowerer, sym)) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json new file mode 100644 index 000000000..45f2b63ab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7614-npm-local-class-per-instance/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7614-npm-local-class-per-instance/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7614-npm-local-class-per-instance/main.ts b/tests/corpus/7614-npm-local-class-per-instance/main.ts new file mode 100644 index 000000000..259c162d0 --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/main.ts @@ -0,0 +1,26 @@ +import { $constructor } from "ctor-kit"; + +const Base = $constructor("Base", (inst: any, def: any) => { + inst.kind = def.kind; +}); +const Failure = $constructor( + "Failure", + (inst: any, def: any) => { + inst.name = "Failure"; + inst.issues = def; + }, + { Parent: Error }, +); +const Coded = $constructor( + "Coded", + (inst: any, def: any) => { + inst.code = def; + }, + { Parent: TypeError }, +); +const base = new Base({ kind: "plain" }); +const failure = new Failure([1, 2]); +const coded = new Coded(7); +console.log(base.kind, base._zod.def.kind, base._zod.constr === Base); +console.log(failure.name, failure.issues.length, failure instanceof Error, failure._zod.constr === Failure); +console.log(coded.code, coded instanceof TypeError, coded instanceof Error, coded._zod.traits.has("Coded")); diff --git a/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts new file mode 100644 index 000000000..3233861c5 --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.d.ts @@ -0,0 +1,5 @@ +export declare function $constructor( + name: string, + initializer: (inst: any, def: any) => void, + params?: { Parent: ErrorConstructor | TypeErrorConstructor }, +): any; diff --git a/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js new file mode 100644 index 000000000..9aca97175 --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/index.js @@ -0,0 +1,34 @@ +// zod 4's `$constructor`, reduced: the factory is lowered once per +// argument-type tuple, and `_`, hoisted above `class Definition` and +// reached early through `Internals`, constructs its own call's class. +const _desc = { value: undefined, enumerable: false }; +function newError(Definition) { + return new Definition(); +} +export function $constructor(name, initializer, params) { + function Internals(def) { + this.def = def; + this.constr = _; + this.traits = new Set(); + } + function init(inst, def) { + if (!inst._zod) { + _desc.value = new Internals(def); + Object.defineProperty(inst, "_zod", _desc); + _desc.value = undefined; + } + inst._zod.traits.add(name); + initializer(inst, def); + } + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? newError(Definition) : this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, "name", { value: name }); + return _; +} diff --git a/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json new file mode 100644 index 000000000..444df971a --- /dev/null +++ b/tests/corpus/7614-npm-local-class-per-instance/node_modules/ctor-kit/package.json @@ -0,0 +1 @@ +{ "name": "ctor-kit", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json b/tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json new file mode 100644 index 000000000..582f2a9d2 --- /dev/null +++ b/tests/harness/corpus-costs/7614-npm-local-class-per-instance/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3502, "san": 4012 } From 2f882e1e1e52e9d3692758d1669f5b67162211fc Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:44:07 -0700 Subject: [PATCH 131/169] Reflect the prototype of function-local classes without prototype members Object.getPrototypeOf on an instance of a function-local class refused unless its prototype members were installed at run time. A class that declares no prototype methods or accessors (zod 4's class Definition extends Parent {}) has nothing to install, so its shared prototype view is returned, as is a bare Local.prototype value. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-prototypes.ts | 24 ++++++++++++- ...3-local-class-prototype-reflection.js.json | 6 ++++ ...ocal-class-prototype-reflection-ts.ts.json | 6 ++++ .../7613-local-class-prototype-reflection.js | 36 +++++++++++++++++++ ...615-local-class-prototype-reflection-ts.ts | 14 ++++++++ ...3-local-class-prototype-reflection.js.json | 1 + ...ocal-class-prototype-reflection-ts.ts.json | 1 + 7 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json create mode 100644 tests/corpus/7613-local-class-prototype-reflection.js create mode 100644 tests/corpus/7615-local-class-prototype-reflection-ts.ts create mode 100644 tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json create mode 100644 tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 2a05cf977..f65509196 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -278,6 +278,16 @@ export function hasClassPrototypeData(info: ClassInfo): boolean { ); } +/** Methods and accessors that live on the class's prototype. */ +function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean { + return decl.members.nodes.some( + (member) => + (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && + !ts.isPrivateIdentifier(member.name) && + !ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + ); +} + /** Populate the shared prototype view before exposing it to reflection. * Methods and accessors are non-enumerable and retain declaration identity. */ export function reflectClassPrototype( @@ -307,7 +317,10 @@ export function reflectClassPrototype( // Local JavaScript factories initialize captured prototype members on // their constructor value, so the shared runtime view is already complete. if (info.localClass && info.runtimePrototypeMembers) return prototype; - if (info.localClass) + // A function-local class declaring no prototype members (zod 4's + // `class Definition extends Parent {}`) has nothing to install: its + // shared runtime view is complete once its base's is. + if (info.localClass && declaresPrototypeMembers(info.decl)) return refuse("prototype reflection on a function-local class is not supported yet"); if (info.base && !info.base.builtinError) { reflectClassPrototype( @@ -533,6 +546,15 @@ export function lowerClassPrototypeData( if (name === "constructor") { return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr)); } + // The whole prototype of a function-local class: the shared view + // Object.getPrototypeOf answers for its instances. + if (name === null && info.localClass) { + const receiver = lowerer.lowerExprExpecting(expr.expression, { + kind: "classval", + className: info.def.name, + }); + return reflectClassPrototype(lowerer, info, locOf(expr), receiver); + } if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..bbf827605 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7613-local-class-prototype-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..229a4e2ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7615-local-class-prototype-reflection-ts.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7613-local-class-prototype-reflection.js b/tests/corpus/7613-local-class-prototype-reflection.js new file mode 100644 index 000000000..61a116569 --- /dev/null +++ b/tests/corpus/7613-local-class-prototype-reflection.js @@ -0,0 +1,36 @@ +// Prototype reflection on instances of a function-local class without +// methods (zod 4's `$constructor` Definition): the prototype is the class's +// shared prototype object, it chains to the parent's, and members defined +// on it reach every instance. +const installed = new WeakSet(); +function factory(name, Parent) { + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + return function make(label) { + const inst = new Definition(); + inst.label = label; + const proto = Object.getPrototypeOf(inst); + console.log(name, proto === Definition.prototype, proto === Object.getPrototypeOf(new Definition())); + if (!installed.has(proto)) { + installed.add(proto); + Object.defineProperty(proto, "describe", { + configurable: true, + enumerable: false, + get() { + return () => `${name}:${this.label}`; + }, + }); + console.log("installed", name); + } + return inst; + }; +} +const makeError = factory("Failure", Error); +const makePlain = factory("Thing", Object); +const a = makeError("a"); +const b = makeError("b"); +const c = makePlain("c"); +console.log(a.describe(), b.describe(), c.describe()); +console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join()); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe")); diff --git a/tests/corpus/7615-local-class-prototype-reflection-ts.ts b/tests/corpus/7615-local-class-prototype-reflection-ts.ts new file mode 100644 index 000000000..92d3b5668 --- /dev/null +++ b/tests/corpus/7615-local-class-prototype-reflection-ts.ts @@ -0,0 +1,14 @@ +// Prototype reflection on an instance of a TypeScript function-local class +// without methods: one shared prototype per class evaluation. +function make(label: string): { label: string } { + class Box { + label = ""; + } + const box = new Box(); + box.label = label; + return box; +} +const a = make("a"); +const b = make("b"); +const pa = Object.getPrototypeOf(a); +console.log(pa === Object.getPrototypeOf(a), pa === Object.getPrototypeOf(b), typeof pa, Object.keys(a).join()); diff --git a/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..9c4d9369a --- /dev/null +++ b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 1967, "san": 2510 } diff --git a/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..2e0a455c2 --- /dev/null +++ b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1 @@ +{ "plain": 1466, "san": 2143 } From c50fb1275e314df67ed6d9d3499fea4682c16ae5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:46:09 -0500 Subject: [PATCH 132/169] Build the native compiler again after the two-counter while loop pass The native compiler spreads only known record shapes, from values whose type no longer includes undefined (SC1090). runPairedWhileLoopsAsFor copied a counter's local read through a non-null assertion, and renamed counters by spreading nodes narrowed to several kinds. It now tests the local against undefined and renames one node kind at a time. The pass rewrites the same loops to the same IR. --- .../compiler/src/ir/paired-while-loops.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/ir/paired-while-loops.ts b/packages/compiler/src/ir/paired-while-loops.ts index e0a994418..c096c34fe 100644 --- a/packages/compiler/src/ir/paired-while-loops.ts +++ b/packages/compiler/src/ir/paired-while-loops.ts @@ -211,7 +211,8 @@ function rewriteFunction(fn: IrFunction): IrFunction { const renamed = new Map(); const fresh: IrLocal[] = []; for (const id of counters) { - const local = locals.get(id)!; + const local = locals.get(id); + if (local === undefined) break; let k = 0; while (used.has(`${local.name}.${k}`)) k++; const next = `${local.name}.${k}`; @@ -219,6 +220,10 @@ function rewriteFunction(fn: IrFunction): IrFunction { renamed.set(id, next); fresh.push({ ...local, id: next }); } + if (renamed.size !== counters.length) { + for (const local of fresh) used.delete(local.id); + continue; + } const candidate = pairedForLoop(loop, renamed); const forLocals = new Map(locals); for (const local of fresh) forLocals.set(local.id, local); @@ -263,9 +268,16 @@ function pairedForLoop( loop: WhileLoop, renamed: ReadonlyMap, ): IrStmt & { kind: "for" } { + // Each copy spreads one node kind. const expr = (e: IrExpr): IrExpr => { const next = mapExprChildren(e, expr, stmt); - if (next.kind === "varRef" || next.kind === "incDec" || next.kind === "assignExpr") { + if (next.kind === "varRef") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "incDec") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "assignExpr") { const id = renamed.get(next.localId); if (id !== undefined) return { ...next, localId: id }; } @@ -273,12 +285,16 @@ function pairedForLoop( }; const stmt = (s: IrStmt): IrStmt => { const next = mapStmtChildren(s, expr, stmt); - if ( - next.kind === "varDecl" || - next.kind === "assign" || - next.kind === "forOf" || - next.kind === "rethrow" - ) { + if (next.kind === "varDecl") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "assign") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "forOf") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } else if (next.kind === "rethrow") { const id = renamed.get(next.localId); if (id !== undefined) return { ...next, localId: id }; } From b6d6eb60aed3849954d9f69cc0ff4607d63a7504 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 01:34:19 -0500 Subject: [PATCH 133/169] Replace a scheduled _construct hook, and keep runtime option values in order Node's constructNT reads _construct on the construction tick, so an assignment after the constructor and before that tick replaces a scheduled construct hook. The assignment was dropped, so the original hook ran. It now lowers to stream.replaceConstruct, which swaps the hook while its tick has not run and has no effect after it or on a stream without a hook. Runtime decodeStrings and defaultEncoding values are read into locals before the constructor evaluates the other option members, while Node evaluates the members in order. A runtime value after a member whose value has effects (a call or an assignment) now refuses instead of reading a value that member could still change. --- .../builtin-apis/streams-and-readline.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../compiler/src/backend/llvm/lib-stream.ts | 3 + .../src/frontend/lowering/lower-stream.ts | 63 +++++++++++++------ .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/validate.ts | 1 + .../7466-stream-construct-assign.mjs.json | 6 ++ .../stream-runtime-option-order.ts.json | 6 ++ packages/runtime/src/scr_stream.c | 13 ++++ packages/runtime/src/scr_stream.h | 4 ++ tests/corpus/7466-stream-construct-assign.mjs | 46 ++++++++++++++ .../stream-runtime-option-order.ts | 28 +++++++++ .../stream-runtime-option-order.ts.txt | 17 +++++ .../7466-stream-construct-assign.mjs.json | 1 + 14 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json create mode 100644 tests/corpus/7466-stream-construct-assign.mjs create mode 100644 tests/diagnostics/stream-runtime-option-order.ts create mode 100644 tests/harness/__snapshots__/stream-runtime-option-order.ts.txt create mode 100644 tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index b88ead1f4..166fc3402 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -33,6 +33,7 @@ `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side. + A `decodeStrings` or `defaultEncoding` value known only at run time is unsupported after an option member whose value has effects (a call or an assignment). In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write). `setDefaultEncoding()` takes any encoding Node knows. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index f31906354..25af12ad6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1768,6 +1768,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setTransform", "stream.setFlush", "stream.setConstruct", + "stream.replaceConstruct", "stream.duplexFromGen", "stream.duplexFromDyn", "stream.duplexifyReadable", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 5fd4af1a9..fdbf13d4f 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -189,6 +189,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.setTransform" || e.fn === "stream.setFlush" || e.fn === "stream.setConstruct" || + e.fn === "stream.replaceConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" ) { @@ -203,6 +204,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setTransform": "t", "stream.setFlush": "l", "stream.setConstruct": "c", + "stream.replaceConstruct": "c", "stream.setWritev": "v", "stream.setWritevDefault": "v", }; @@ -214,6 +216,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush", "stream.setConstruct": "scr_stream_set_construct", + "stream.replaceConstruct": "scr_stream_replace_construct", "stream.setWritev": "scr_stream_set_writev", "stream.setWritevDefault": "scr_stream_set_writev_default", }; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 15afb741e..cf9f19b9d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -106,6 +106,21 @@ function pureRead(node: ts.Expression): boolean { return false; } +/** Whether every option member before `prop` evaluates without effects. + * Runtime decodeStrings and defaultEncoding values are read into locals + * before the constructor evaluates the other members, while Node evaluates + * the members in order: an earlier member with effects could change them. */ +function earlierOptionsPure(options: ts.ObjectLiteralExpression, prop: ts.Node): boolean { + for (const member of options.properties.nodes) { + if (member === prop) return true; + if (ts.isPropertyAssignment(member)) { + if (!sideEffectFreeOptionValue(member.initializer) && !pureRead(member.initializer)) + return false; + } else if (ts.isSpreadAssignment(member)) return false; + } + return true; +} + /** The stream sides of a receiver class: the nearest stream-class * ancestor's, or null off the stream hierarchy. */ export function streamSidesOf( @@ -1021,6 +1036,16 @@ function parseStreamOptions( break; } // a runtime value: WritableState's `options.decodeStrings !== false` + if ( + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + !earlierOptionsPure(optsLit, prop) + ) + lowerer.noLowering( + `the ${ctorName} option 'decodeStrings' with a runtime value after an option value with effects`, + prop, + "compute the earlier option values into consts before the options object (the runtime value is read before the other options run)", + ); const local = lowerer.declareHiddenLocal("%decodeStrings", DYN); out.valueDecls.push({ kind: "varDecl", @@ -1076,6 +1101,16 @@ function parseStreamOptions( // a runtime value: null, undefined and utf8 are the default, // another name Buffer.isEncoding knows is kept as given, and any // other value throws ERR_UNKNOWN_ENCODING before construction + if ( + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + !earlierOptionsPure(optsLit, prop) + ) + lowerer.noLowering( + `the ${ctorName} option 'defaultEncoding' with a runtime value after an option value with effects`, + prop, + "compute the earlier option values into consts before the options object (the runtime value is read before the other options run)", + ); const local = lowerer.declareHiddenLocal("%defaultEncoding", DYN); const ref = varRef(local.id, DYN, loc); out.valueDecls.push( @@ -1869,24 +1904,15 @@ export function lowerStreamUnderscoreAssign( const loc = locOf(expr); const cls = base.def.name; const clsName = cls.slice(1); - if (methodName === "_construct") { - // Node's constructor reads _construct once; a later assignment has no - // effect on the stream, so only the receiver and the value are - // evaluated - const value = lowerer.lowerExpr(expr.right); - return { - kind: "exprStmt", - expr: { - kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: recv, loc }], - result: value, - type: value.type, - loc, - }, - loc, - }; - } - const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; + // `_construct`: Node's constructNT reads it on the construction tick, so + // an assignment before that tick replaces a scheduled hook + // (stream.replaceConstruct) and has no effect after it or without one. + const which = + methodName === "_writev" + ? "writev" + : methodName === "_construct" + ? "construct" + : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) { lowerer.noLowering( @@ -1925,6 +1951,7 @@ export function lowerStreamUnderscoreAssign( transform: "stream.setTransform", flush: "stream.setFlush", writev: "stream.setWritev", + construct: "stream.replaceConstruct", }; return { kind: "exprStmt", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index aa31105e7..ca91b399c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3975,6 +3975,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.setTransform": { argTypes: [null, null], result: VOID }, "stream.setFlush": { argTypes: [null, null], result: VOID }, "stream.setConstruct": { argTypes: [null, null], result: VOID }, + "stream.replaceConstruct": { argTypes: [null, null], result: VOID }, "stream.setWritev": { argTypes: [null, null], result: VOID }, "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, /** The dyn-options twins (a checked-dynamic options record — the JS diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 44c032620..8b3b6c048 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5508,6 +5508,7 @@ function validateFunction( e.fn === "stream.duplexFromGenSignal" || e.fn === "stream.duplexFromAsyncFnSignal" || e.fn === "stream.setConstruct" || + e.fn === "stream.replaceConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" || e.fn === "stream.iteratorClose" || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json new file mode 100644 index 000000000..f5bb52517 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7466-stream-construct-assign.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json new file mode 100644 index 000000000..40b64edac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/stream-runtime-option-order.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 4b962675a..cd95e12cd 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -269,6 +269,7 @@ struct ScrStreamState { * destroyed and runs its _destroy once construction completes. */ bool constructing, destroy_deferred; bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */ + bool construct_called; /* the construct tick called the hook */ ScrError *destroy_err; /* owned: the deferred destroy's error */ ScrClosure *construct_cb; ScrStreamPlainInv construct_inv; @@ -7523,6 +7524,17 @@ void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamP scr_stream_construct(s, cb, inv); } +void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) { + ScrStreamState *st = s->st; + if (!st->constructing || st->construct_called || !st->construct_cb) { + scr_closure_release(cb); + return; + } + scr_closure_release(st->construct_cb); + st->construct_cb = cb; + st->construct_inv = inv; +} + /* A class's _writev under options that may carry their own writev (the * option, an instance property in Node, wins). */ void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { @@ -7791,6 +7803,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { scr_stream_error_buffer(s); break; case SCR_ST_CONSTRUCT: { + st->construct_called = true; if (st->construct_cb) { st->construct_inv(st->construct_cb, s); /* a throw inside _construct reaches onConstruct on a tick */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index e368ec7ff..a4798c629 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -561,6 +561,10 @@ void scr_stream_set_writev_concat(ScrStream *s); /* The user construct option / _construct method (scheduled now; cb * moves), and the writev option / _writev method (cb moves). */ void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); +/* `s._construct = fn` after construction: replaces a construct hook whose + * tick has not run yet (Node's constructNT reads _construct then), and has + * no effect otherwise (cb moves). */ +void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); /* A compiled writev whose entries declare `encoding` a string: entries diff --git a/tests/corpus/7466-stream-construct-assign.mjs b/tests/corpus/7466-stream-construct-assign.mjs new file mode 100644 index 000000000..37684afcd --- /dev/null +++ b/tests/corpus/7466-stream-construct-assign.mjs @@ -0,0 +1,46 @@ +// Assigning _construct after construction: Node reads it on the +// construction tick, so an assignment before that tick replaces the +// scheduled hook, and one after it, or on a stream without a hook, has no +// effect. +import { Readable, Writable } from "node:stream"; + +const w = new Writable({ + construct(cb) { + console.log("original construct"); + cb(); + }, + write(chunk, _enc, cb) { + console.log("write", chunk.toString()); + cb(); + }, +}); +w._construct = function (cb) { + console.log("replacement construct"); + cb(); +}; +w.write("a"); +w.end(); +w.on("finish", () => { + w._construct = function (cb) { + console.log("late construct"); + cb(); + }; + console.log("finish"); + readable(); +}); + +// A stream without a construct hook: the assignment has no effect. +function readable() { + const r = new Readable({ + read() { + this.push("x"); + this.push(null); + }, + }); + r._construct = function (cb) { + console.log("never"); + cb(); + }; + r.on("data", (d) => console.log("data", d.toString())); + r.on("end", () => console.log("end")); +} diff --git a/tests/diagnostics/stream-runtime-option-order.ts b/tests/diagnostics/stream-runtime-option-order.ts new file mode 100644 index 000000000..b3854e328 --- /dev/null +++ b/tests/diagnostics/stream-runtime-option-order.ts @@ -0,0 +1,28 @@ +import { Writable } from "node:stream"; + +// Runtime decodeStrings and defaultEncoding values are read before the +// other option members are evaluated, while Node evaluates the members in +// order: a value after a member with effects refuses. +let encoding = (process.env["STREAM_ENCODING"] ?? "utf8") as BufferEncoding; +let decode = process.env["STREAM_DECODE"] !== "0"; +function bump(): number { + encoding = "hex"; + decode = false; + return 16; +} +const sink = new Writable({ + highWaterMark: bump(), + defaultEncoding: encoding, + write(_chunk, _encoding, done) { + done(); + }, +}); +const other = new Writable({ + highWaterMark: bump(), + decodeStrings: decode, + write(_chunk, _encoding, done) { + done(); + }, +}); +sink.end(); +other.end(); diff --git a/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt b/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt new file mode 100644 index 000000000..8e08dfbf0 --- /dev/null +++ b/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt @@ -0,0 +1,17 @@ +stream-runtime-option-order.ts:15:3 - error SC2020: 'the Writable option 'defaultEncoding' with a runtime value after an option value with effects' is part of the standard library types but has no scriptc lowering yet + + 14 | highWaterMark: bump(), + 15 | defaultEncoding: encoding, + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 16 | write(_chunk, _encoding, done) { + + hint: compute the earlier option values into consts before the options object (the runtime value is read before the other options run) + +stream-runtime-option-order.ts:22:3 - error SC2020: 'the Writable option 'decodeStrings' with a runtime value after an option value with effects' is part of the standard library types but has no scriptc lowering yet + + 21 | highWaterMark: bump(), + 22 | decodeStrings: decode, + | ^~~~~~~~~~~~~~~~~~~~~ + 23 | write(_chunk, _encoding, done) { + + hint: compute the earlier option values into consts before the options object (the runtime value is read before the other options run) \ No newline at end of file diff --git a/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json b/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json new file mode 100644 index 000000000..1c67bb43f --- /dev/null +++ b/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json @@ -0,0 +1 @@ +{ "plain": 1571, "san": 2074 } From 14ba361985d6003636d6b654a8a58346b73c89e4 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:44:06 -0700 Subject: [PATCH 134/169] Call own accessors over an Error's name and message from typed code zod 4 defines its error message with Object.defineProperty(inst, "message", { get, set }). The typed-ref commit of an Error subclass read the accessor as a data value and threw "expected string". The commit and the slot sync now leave the slot alone and mark the property absent; typed reads and writes of name and message call the accessor when the program reflects on an untyped or Error-shaped target. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/frontend/lowering/error-methods.ts | 52 +++++++- .../src/frontend/lowering/lower-calls.ts | 25 ++++ .../compiler/src/frontend/lowering/lowerer.ts | 5 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 8 ++ packages/compiler/src/ir/validate.ts | 3 + .../7611-error-message-accessor.ts.json | 6 + .../7612-error-message-accessor-lazy.js.json | 6 + packages/runtime/src/scr_error.h | 3 + packages/runtime/src/scr_json.c | 112 ++++++++++++++---- tests/corpus/7611-error-message-accessor.ts | 43 +++++++ .../7612-error-message-accessor-lazy.js | 36 ++++++ .../7611-error-message-accessor.ts.json | 1 + .../7612-error-message-accessor-lazy.js.json | 1 + 15 files changed, 283 insertions(+), 24 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json create mode 100644 tests/corpus/7611-error-message-accessor.ts create mode 100644 tests/corpus/7612-error-message-accessor-lazy.js create mode 100644 tests/harness/corpus-costs/7611-error-message-accessor.ts.json create mode 100644 tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 054d9341b..d88abbc68 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -134,6 +134,9 @@ export const LIB_FN_SYMS: Record = { "error.setCodeDyn": "scr_error_set_code_dyn", "error.assign": "scr_error_assign_dyn", "error.hasKey": "scr_error_has_key", + "error.ownAccessor": "scr_error_own_accessor", + "error.accessorGet": "scr_error_accessor_get", + "error.accessorSet": "scr_error_accessor_set", "error.hasCode": "scr_error_has_code", "error.defineCause": "scr_error_define_cause", "error.deleteCause": "scr_error_delete_cause", diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index b545f4147..067f9c286 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -94,12 +94,15 @@ export function refreshErrorPropertyDispatch( lowerer.isSubclassOf(info.def.name, "%Error"), ); const properties: ("name" | "message")[] = ["name", "message"]; + // Test lowerers built from partial objects leave the location undefined. + const accessors = Boolean(lowerer.errorAccessorReflection); for (const property of properties) { if ( - overrides.length && + (overrides.length || accessors) && !lowerer.liftedFns.some((fn) => fn.name === `%error.${property}.read`) ) { - errorPropertyRead(lowerer, varRef("error.0", errorType, overrides[0]!.def.loc), property); + const loc = overrides[0]?.def.loc ?? lowerer.errorAccessorReflection!; + errorPropertyRead(lowerer, varRef("error.0", errorType, loc), property); return true; } } @@ -115,6 +118,7 @@ export function refreshErrorPropertyDispatch( const revision = JSON.stringify([ readers.map((fn) => fn.name), overrides.map((info) => [info.def.name, hasClassPrototypeData(info)]), + accessors, ]); if (lowerer.errorPropertyDispatchRevision === revision) return false; lowerer.errorPropertyDispatchRevision = revision; @@ -183,6 +187,50 @@ export function refreshErrorPropertyDispatch( }, ]; const body: IrStmt[] = [{ kind: "if", cond: own, then: fallback, else_: null, loc }]; + // An own accessor a reflective definition installed (the slot reads as + // absent then): the read calls its getter, the write its setter. + if (accessors) + body.push({ + kind: "if", + cond: { kind: "libCall", fn: "error.ownAccessor", args: [receiver, key], type: BOOL, loc }, + then: write + ? [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "error.accessorSet", + args: [ + receiver, + key, + lowerer.coerceToExpected(varRef("value", STRING, loc), DYN), + ], + type: VOID, + loc, + }, + loc, + }, + { kind: "return", value: null, loc }, + ] + : [ + { + kind: "return", + value: lowerer.coerceToExpected( + { + kind: "libCall", + fn: "error.accessorGet", + args: [receiver, key], + type: DYN, + loc, + }, + STRING, + ), + loc, + }, + ], + else_: null, + loc, + }); helper.locals = helper.locals.filter((local) => !local.id.startsWith("descriptor.")); for (const info of overrides) { const member = `${write ? "set" : "get"}:${property}`; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index afae96b26..54bf9a30d 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -267,8 +267,33 @@ import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generati const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); +/** `Object.defineProperty(target, ...)`, `Object.defineProperties`, + * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` + * over an untyped or Error-shaped target can install an accessor over an + * error's `name` or `message` (error-methods.ts). */ +function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): void { + const callee = expr.expression; + if (!ts.isPropertyAccessExpression(callee)) return; + const member = callee.name.text; + const target = + member === "defineProperty" || member === "defineProperties" + ? expr.arguments.nodes[0] + : member === "__defineGetter__" || member === "__defineSetter__" + ? callee.expression + : undefined; + if (!target) return; + const type = lowerer.checker.getTypeAtLocation(target); + if ( + type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) || + (lowerer.checker.getPropertyOfType(type, "message") && + lowerer.checker.getPropertyOfType(type, "name")) + ) + lowerer.errorAccessorReflection = locOf(expr); +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); + if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); // Object's checker return type is `any`. Claim known object inputs // before generic checked-dynamic dispatch tries to load the constructor. if ( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4c417e86e..f6b23f13c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3207,6 +3207,11 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; + /** The program defines properties by reflection on an untyped or + * Error-shaped target, so an own accessor may replace an error's `name` + * or `message`: typed reads and writes of those slots check for one. + * Holds the first such call's location. */ + errorAccessorReflection: SrcLoc | null = null; /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ /** Files are named by their path relative to the entry's directory, so diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d8e78be08..69766374a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -446,6 +446,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "global.undefRead", // The compiler-resolved Node-parity throw: always throws, catchably. "error.nodeThrow", + // An accessor over an error's own name or message runs user code. + "error.accessorGet", + "error.accessorSet", // USVString coercion runs user toString/valueOf — throws propagate. "dyn.toStringCoerce", "dyn.stringConstructor", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7cb666ccf..0ab55e0c4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3429,6 +3429,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * throw-site fields, dynamic writes) or an Object.prototype member. */ "error.hasKey": { argTypes: [null, STRING], result: BOOL }, "error.hasCode": { argTypes: [null], result: BOOL }, + /** Whether the error's own `name` or `message` (the key) is an accessor + * a reflective definition installed. Borrows both; never throws. */ + "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, + /** That accessor's getter called on the error (owned result), and its + * setter called with the value (V8's TypeError without one). Borrow + * their arguments; throw what the accessor throws. */ + "error.accessorGet": { argTypes: [null, STRING], result: DYN }, + "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, "error.defineCause": { argTypes: [null, DYN], result: VOID }, "error.deleteCause": { argTypes: [null], result: VOID }, /** The compiler-resolved Node-parity throw for always-throwing lowered diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 770fee6c6..d1ce8cdf0 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4144,6 +4144,9 @@ function validateFunction( e.fn === "error.assign" || e.fn === "error.hasKey" || e.fn === "error.hasCode" || + e.fn === "error.ownAccessor" || + e.fn === "error.accessorGet" || + e.fn === "error.accessorSet" || e.fn === "error.defineCause" || e.fn === "error.deleteCause" ) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..ef262841b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7611-error-message-accessor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bc852b7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7612-error-message-accessor-lazy.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index a7c921f23..fbb64aad9 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -296,6 +296,9 @@ void scr_error_assign_dyn(ScrError *e, const struct ScrDyn *sources); /* `key in err` for a key no compiled class declares: a runtime-set own * property or an Object.prototype member. Borrows both (scr_json.c). */ bool scr_error_has_key(ScrError *e, const ScrStr *key); +bool scr_error_own_accessor(ScrError *e, const ScrStr *key); +struct ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key); +void scr_error_accessor_set(ScrError *e, const ScrStr *key, struct ScrDyn *value); bool scr_error_has_code(ScrError *e); const char *scr_error_message_code(const ScrStr *msg); ScrStr *scr_error_code(ScrError *e); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3fd57da73..9cf474d75 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8010,35 +8010,36 @@ ScrDyn *scr_errdyn_view_of(const ScrError *e) { static void scr_error_cause_drop_impl(void *obj); void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { - ScrDynEntry *name_entry = NULL; - for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 4 && memcmp(entry->key, "name", 4) == 0) { name_entry = entry; break; } - } - ScrDyn *name = name_entry ? name_entry->value : NULL; + ScrDynEntry *name_entry = scr_dyn_find_entry(view, "name", 4); + ScrDynEntry *message_entry = scr_dyn_find_entry(view, "message", 7); + /* An own accessor over `name` or `message` (zod's lazy error message) + * has no value to snapshot: the slot keeps its last value, and the + * property reads as absent so typed reads take the accessor path + * (scr_error_own_accessor) and toString asks the installed reader. */ + ScrDyn *name = name_entry && !name_entry->accessor ? name_entry->value : NULL; if (name && name->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", name); return; } - ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); - scr_str_release(e->name); - e->name = next_name; - e->name_present = name != NULL; - e->name_enumerable = name_entry && name_entry->enumerable; - ScrDyn *message = scr_dyn_obj_get(view, "message", 7); + ScrDyn *message = message_entry && !message_entry->accessor ? message_entry->value : NULL; if (message && message->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", message); return; } - ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); - scr_str_release(e->message); - e->message = next_message; - e->message_present = message != NULL; - e->message_enumerable = false; - for (size_t i = 0; message && i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0) e->message_enumerable = entry->enumerable; + if (!name_entry || !name_entry->accessor) { + ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); + scr_str_release(e->name); + e->name = next_name; } + e->name_present = name != NULL; + e->name_enumerable = name_entry && name_entry->enumerable; + if (!message_entry || !message_entry->accessor) { + ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); + scr_str_release(e->message); + e->message = next_message; + } + e->message_present = message != NULL; + e->message_enumerable = message_entry && message_entry->enumerable; ScrDyn *stack = scr_dyn_obj_get(view, "stack", 5); if (stack && stack->kind == SCR_DYN_STR) { ScrStr *replacement = scr_str_retain(stack->v.str); @@ -8134,8 +8135,20 @@ static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr * return; } if (name || message) { - /* The native error's toString and inspect read these fields. */ - if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return; + /* The native error's toString and inspect read these fields. An own + * accessor leaves the slot, which then reads as absent: typed reads + * call it (scr_error_own_accessor). */ + if (entry && entry->accessor) { + if (name) { + e->name_present = false; + e->name_enumerable = entry->enumerable; + } else { + e->message_present = false; + e->message_enumerable = entry->enumerable; + } + return; + } + if (!entry || entry->value->kind != SCR_DYN_STR) return; ScrStr **field = name ? &e->name : &e->message; scr_str_release(*field); *field = scr_str_retain(entry->value->v.str); @@ -16044,6 +16057,61 @@ bool scr_error_has_code(ScrError *e) { * property set at runtime, which lives on the error's checked-dynamic node * (that node exists once anything reflected or wrote through it), or a * member of Object.prototype. Borrows both; never throws. */ +/* Whether the error's own `name` or `message` (the key) is an accessor a + * reflective definition installed on its checked-dynamic node (the slot + * then reads as absent): typed reads call its getter and typed writes its + * setter through that node. A class view keeps the accessor in its last + * materialized object. Borrows both; never throws. */ +static ScrDynEntry *scr_error_accessor_entry(ScrError *e, const ScrStr *key) { + const ScrDyn *view = scr_errdyn_view_of(e); + if (view != NULL && view->kind == SCR_DYN_TYPED_REF) view = view->v.typed_ref.materialized; + if (view == NULL || view->kind != SCR_DYN_OBJ) return NULL; + ScrDynEntry *entry = scr_dyn_find_entry(view, key->data, key->len); + return entry != NULL && entry->accessor ? entry : NULL; +} + +bool scr_error_own_accessor(ScrError *e, const ScrStr *key) { return scr_error_accessor_entry(e, key) != NULL; } + +/* A typed read of that accessor: its getter called on the error's node + * (the undefined value without one). Owned result, or NULL with the + * getter's exception pending. */ +ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL || entry->getter == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *getter = scr_dyn_retain(entry->getter); + ScrDyn *receiver = scr_dyn_from_error(e); + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); + scr_dyn_this_pop(); + scr_dyn_release(receiver); + scr_dyn_release(getter); + return result; +} + +/* A typed write of that accessor: its setter called on the error's node + * with the value, or V8's strict-mode TypeError without one. Borrows all; + * the setter's exception stays pending. */ +void scr_error_accessor_set(ScrError *e, const ScrStr *key, ScrDyn *value) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL) return; + if (entry->setter == NULL) { + char message[256]; + int n = snprintf(message, sizeof message, "Cannot set property %.*s of %.*s which has only a getter", + (int)key->len, key->data, (int)e->name->len, e->name->data); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n < sizeof message ? (size_t)n : sizeof message - 1); + return; + } + ScrDyn *setter = scr_dyn_retain(entry->setter); + ScrDyn *receiver = scr_dyn_from_error(e); + ScrDyn *args[1] = {value}; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(setter, args, 1, "setter"); + scr_dyn_this_pop(); + scr_dyn_release(result); + scr_dyn_release(receiver); + scr_dyn_release(setter); +} + bool scr_error_has_key(ScrError *e, const ScrStr *key) { const ScrDyn *view = scr_errdyn_view_of(e); if (view != NULL && view->kind == SCR_DYN_OBJ && scr_dyn_find_entry(view, key->data, key->len) != NULL) diff --git a/tests/corpus/7611-error-message-accessor.ts b/tests/corpus/7611-error-message-accessor.ts new file mode 100644 index 000000000..c4528e488 --- /dev/null +++ b/tests/corpus/7611-error-message-accessor.ts @@ -0,0 +1,43 @@ +// An accessor defined over an Error subclass instance's own `message` +// (zod 4's lazy error message): typed reads and writes, String(), and +// reads through an untyped alias all go through the accessor, and a later +// data definition replaces it. +class E extends Error {} +const e = new E("orig"); +let store = "lazy"; +let reads = 0; +Object.defineProperty(e, "message", { + get() { + reads++; + return store; + }, + set(value: string) { + store = `set:${value}`; + }, + enumerable: true, + configurable: true, +}); +console.log(e.message, String(e), reads); +e.message = "typed"; +console.log(e.message, store); +const loose: any = e; +console.log(loose.message); +loose.message = "loose"; +console.log(e.message, Object.keys(e).join()); +const desc = Object.getOwnPropertyDescriptor(e, "message")!; +console.log(typeof desc.get, typeof desc.set, desc.enumerable); +Object.defineProperty(e, "message", { value: "data", writable: true, configurable: true }); +console.log(e.message, String(e), reads); +e.message = "after"; +console.log(e.message, store); + +class Named extends Error {} +const n = new Named("m"); +const loosen: any = n; +Object.defineProperty(loosen, "name", { get() { return "Custom"; }, configurable: true }); +console.log(n.name, String(n)); + +class Plain extends Error {} +const p = new Plain("kept"); +p.message = "changed"; +console.log(p.message, String(p)); diff --git a/tests/corpus/7612-error-message-accessor-lazy.js b/tests/corpus/7612-error-message-accessor-lazy.js new file mode 100644 index 000000000..26d54c09c --- /dev/null +++ b/tests/corpus/7612-error-message-accessor-lazy.js @@ -0,0 +1,36 @@ +// zod 4's lazy error message: an initializer defines a shared accessor +// descriptor over `message` on an instance whose class extends a runtime +// Error parent, and the message is computed on first read and cached. +function _getMessage() { + const internals = this._zod; + internals.message ?? (internals.message = JSON.stringify(internals.def)); + return internals.message; +} +function _setMessage(value) { + this._zod.message = value; +} +const _messageDesc = { get: _getMessage, set: _setMessage, enumerable: true, configurable: true }; +function initializer(inst, def) { + Object.defineProperty(inst, "_zod", { value: { def }, enumerable: false }); + inst.name = "$ZodError"; + Object.defineProperty(inst, "message", _messageDesc); +} +function make(Parent, def) { + class Definition extends Parent {} + const inst = new Definition(); + initializer(inst, def); + return inst; +} +const err = make(Error, [{ code: "too_small", path: ["a"] }]); +console.log(err._zod.message, err.message, err._zod.message === err.message); +console.log(String(err), Object.keys(err).join()); +err.message = "replaced"; +console.log(err.message, err._zod.message, `${err}`); +try { + throw err; +} catch (caught) { + console.log(caught === err, caught.message, caught instanceof Error); +} +const plain = new Error("start"); +Object.defineProperty(plain, "message", { get() { return "computed"; }, configurable: true }); +console.log(plain.message, String(plain)); diff --git a/tests/harness/corpus-costs/7611-error-message-accessor.ts.json b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..8c97e80f9 --- /dev/null +++ b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json @@ -0,0 +1 @@ +{ "plain": 2962, "san": 3600 } diff --git a/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bbc81f519 --- /dev/null +++ b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json @@ -0,0 +1 @@ +{ "plain": 2217, "san": 2721 } From 1d68edc0f6063ee0b2e288dbf6e02a66f7536d41 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 01:39:26 -0500 Subject: [PATCH 135/169] Give function-local class prototypes their own constructor The prototype object of a function-local class, created once per evaluation of the class, now starts with its own non-enumerable `constructor`, the class value of that evaluation, as every class prototype does. Reflecting the prototype of a function-local class without prototype members exposed an object without it: `proto.constructor` answered the base class, and `Object.hasOwn(proto, "constructor")` and its property descriptor did not match Node. --- packages/runtime/src/scr_json.c | 5 +++++ .../7613-local-class-prototype-reflection.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3fd57da73..22066e186 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4415,6 +4415,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { * the class object and no registry keeps it alive, so evaluating a * function-local class again releases the previous one's prototype. */ if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data; + /* Like every class prototype, it starts with its own non-enumerable + * `constructor`: the class value. The cycle it closes through the class + * object is one the collector traces. */ + scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value)); + data->v.obj.entries[data->v.obj.len - 1].enumerable = false; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } diff --git a/tests/corpus/7613-local-class-prototype-reflection.js b/tests/corpus/7613-local-class-prototype-reflection.js index 61a116569..a48bd305c 100644 --- a/tests/corpus/7613-local-class-prototype-reflection.js +++ b/tests/corpus/7613-local-class-prototype-reflection.js @@ -34,3 +34,22 @@ console.log(a.describe(), b.describe(), c.describe()); console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join()); console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe")); + +// The prototype has its own non-enumerable `constructor`, the class value +// of that evaluation, as every class prototype does. +function local(Parent) { + class Definition extends Parent {} + const proto = Object.getPrototypeOf(new Definition()); + const descriptor = Object.getOwnPropertyDescriptor(proto, "constructor"); + console.log( + proto.constructor === Definition, + Object.hasOwn(proto, "constructor"), + JSON.stringify(Object.getOwnPropertyNames(proto)), + descriptor.value === Definition, + descriptor.writable, + descriptor.enumerable, + descriptor.configurable, + ); + return Definition; +} +console.log(local(Object) !== local(Error)); From 14a2769f58d0ccafbb89effec4f4d17234bf11ae Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:18:02 -0700 Subject: [PATCH 136/169] Create untyped class instances' property-bag entries without the class tables Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/class-dynamic-dispatch.ts | 315 ++++++++++++++---- .../compiler/src/frontend/lowering/lowerer.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 4 + packages/runtime/src/scr_json.c | 15 + 5 files changed, 269 insertions(+), 69 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 4a9578e5e..88425ca66 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1553,6 +1553,7 @@ export const LIB_FN_SYMS: Record = { "dyn.assignPrototype": "scr_dyn_assign_prototype", "dyn.definePrototypeProps": "scr_dyn_define_prototype_props", "dyn.bagGet": "scr_dyn_bag_get", + "dyn.bagCreateField": "scr_dyn_bag_create_field", "dyn.bagSet": "scr_dyn_bag_set", "dyn.objCreate": "scr_dyn_obj_create", "dyn.nameClassPrototype": "scr_dyn_name_class_prototype", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index e394718d1..b5b585989 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -685,16 +685,154 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[ if (![...lowerer.classes.values()].some((info) => info.def.tracksOwnFields)) return; const loc = functions[0]!.loc; const name = "%dyn.class.fieldCreated"; - const value = varRef("value", DYN, loc), - key = varRef("key", DYN, loc); - const bag = varRef("bag", DYN, loc); - const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => ({ + const key = varRef("key", DYN, loc); + // One helper per class: the instance's bag is its own hidden field, so a + // creation reads it directly instead of boxing the instance and testing + // it against every class's capsule (%dyn.class.properties), which runs + // only to create the bag, once per instance. + const perClass = new Map(); + const helperFor = (className: string): string => { + const known = perClass.get(className); + if (known !== undefined) return known; + const helperName = `${name}:${className}`; + perClass.set(className, helperName); + const self = varRef("this", { kind: "object", className }, loc); + const bagRef = varRef("bag", DYN, loc); + functions.push({ + name: helperName, + params: [ + { localId: "this", name: "this", type: { kind: "object", className } }, + { localId: "key", name: "key", type: DYN }, + ], + returnType: VOID, + locals: [ + { id: "this", name: "this", type: { kind: "object", className }, mutable: false }, + { id: "key", name: "key", type: DYN, mutable: false }, + { id: "bag", name: "bag", type: DYN, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "bag", + init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc }, + then: initializeBag(lowerer, className, self, loc), + else_: null, + loc, + }, + ...defineIfAbsent(lowerer, bagRef, key, loc), + ], + loc, + }); + return helperName; + }; + // String-keyed fields take the runtime's direct insert for an ordinary + // bag; the general path stays for any other bag and for symbol keys. + const stringHelperFor = (className: string): string => { + const known = perClass.get(`str:${className}`); + if (known !== undefined) return known; + const general = helperFor(className); + const helperName = `${name}.string:${className}`; + perClass.set(`str:${className}`, helperName); + const type: IrType = { kind: "object", className }; + const self = varRef("this", type, loc); + const bagRef = varRef("bag", DYN, loc); + const field = varRef("field", STRING, loc); + functions.push({ + name: helperName, + params: [ + { localId: "this", name: "this", type }, + { localId: "field", name: "field", type: STRING }, + ], + returnType: VOID, + locals: [ + { id: "this", name: "this", type, mutable: false }, + { id: "field", name: "field", type: STRING, mutable: false }, + { id: "bag", name: "bag", type: DYN, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "bag", + init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc }, + then: initializeBag(lowerer, className, self, loc), + else_: null, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.bagCreateField", + args: [bagRef, field], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "call", + callee: general, + args: [self, lowerer.coerceToExpected(field, DYN)], + type: VOID, + loc, + }, + loc, + }, + ], + loc, + }); + return helperName; + }; + const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => { + const symbolKey = symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc); + if (symbolKey === null) { + return { + kind: "exprStmt", + expr: { + kind: "call", + callee: stringHelperFor(className), + args: [ + receiver.type.kind === "object" && receiver.type.className !== className + ? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc } + : receiver, + { kind: "strLit", value: field, type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }; + } + return createdGeneral(receiver, className, field, loc); + }; + const createdGeneral = ( + receiver: IrExpr, + className: string, + field: string, + loc: SrcLoc, + ): IrStmt => ({ kind: "exprStmt", expr: { kind: "call", - callee: name, + callee: helperFor(className), args: [ - lowerer.coerceToExpected(receiver, DYN), + receiver.type.kind === "object" && receiver.type.className !== className + ? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc } + : receiver, symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc) ?? lowerer.coerceToExpected({ kind: "strLit", value: field, type: STRING, loc }, DYN), ], @@ -769,80 +907,103 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[ }, }); } - functions.push({ - name, - params: [ - { localId: "value", name: "value", type: DYN }, - { localId: "key", name: "key", type: DYN }, - ], - returnType: VOID, - locals: [ - { id: "value", name: "value", type: DYN, mutable: false }, - { id: "key", name: "key", type: DYN, mutable: false }, - { id: "bag", name: "bag", type: DYN, mutable: false }, - ], - body: [ +} + +/** `bag = `, stored in the instance's hidden field: + * the class's own initializer when recorded, else the class-table + * dispatch, which creates it. */ +function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: SrcLoc): IrStmt[] { + const init = lowerer.classBagInitializers?.get(className); + if (init === undefined) { + return [ { - kind: "varDecl", + kind: "assign", localId: "bag", - init: { + value: { kind: "call", callee: classPropertiesHelper(lowerer, loc).name, - args: [value], + args: [lowerer.coerceToExpected(self, DYN)], type: DYN, loc, }, loc, }, - { - kind: "if", - cond: { - kind: "unary", - op: "!", - operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, - type: BOOL, - loc, - }, - then: [ - { - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.defineProperty", - args: [ - bag, - lowerer.coerceToExpected(key, DYN), - { - kind: "dynObjLit", - fields: [ - { - key: { kind: "strLit", value: "value", type: STRING, loc }, - value: dynUndefinedExpr(loc), - }, - ...["writable", "enumerable", "configurable"].map((key) => ({ - key: { kind: "strLit" as const, value: key, type: STRING, loc }, - value: lowerer.coerceToExpected( - { kind: "boolLit", value: true, type: BOOL, loc }, - DYN, - ), - })), - ], - type: DYN, - loc, - }, - ], - type: DYN, - loc, - }, - loc, - }, - ], - else_: null, + ]; + } + return [ + { + kind: "fieldSet", + obj: self, + className, + field: PROPERTY_BAG, + value: structuredClone(init), + loc, + }, + { + kind: "assign", + localId: "bag", + value: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + ]; +} + +/** Whether `value` reads local `id`. */ +function mentionsLocal(value: IrExpr, id: string): boolean { + return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`); +} + +/** `if (!hasOwn(bag, key)) defineProperty(bag, key, )`: + * a field's creation in its instance's property bag. */ +function defineIfAbsent(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrStmt[] { + return [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, + type: BOOL, loc, }, - ], - loc, - }); + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.defineProperty", + args: [ + bag, + lowerer.coerceToExpected(key, DYN), + { + kind: "dynObjLit", + fields: [ + { + key: { kind: "strLit", value: "value", type: STRING, loc }, + value: dynUndefinedExpr(loc), + }, + ...["writable", "enumerable", "configurable"].map((key) => ({ + key: { kind: "strLit" as const, value: key, type: STRING, loc }, + value: lowerer.coerceToExpected( + { kind: "boolLit", value: true, type: BOOL, loc }, + DYN, + ), + })), + ], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ]; } /** Calls on native class capsules keep the instance's compiled methods. The @@ -4516,6 +4677,7 @@ export class ClassDynamicDispatch { finalize(lowerer?: Lowerer): void { // Each table serves every site that reaches it. if (lowerer) for (const fn of this.generated) lowerer.shareHelper(fn); + if (lowerer) this.recordBagInitializers(lowerer); for (const { helper, initialize, orEmpty } of this.readPropertyBags) { const absent = helper.body[0]; if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift(); @@ -4538,6 +4700,21 @@ export class ClassDynamicDispatch { } } + /** Each tracked class's bag initializer over a typed receiver (`this`), + * for the per-class field-creation helpers (trackClassFieldCreation), + * which create an instance's bag without the class-table dispatch. */ + private recordBagInitializers(lowerer: Lowerer): void { + for (const [className, initializers] of this.bagInitializers) { + const init = initializers[0]; + if (!init || !lowerer.classes.get(className)?.def.tracksOwnFields) continue; + const self = varRef("this", { kind: "object", className }, init.loc); + const value = withTypedReceiver(structuredClone(init.value), self); + if (mentionsLocal(value, "p.0")) continue; + // Lowerer stand-ins in unit tests may not carry the table. + lowerer.classBagInitializers?.set(className, value); + } + } + private normalizedDispatches(): IrFunction[] { return [ ...(this.propertyBag ? [this.propertyBag] : []), diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a765c91ee..66d3fd294 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3646,6 +3646,9 @@ export class Lowerer { .some((sf) => isDeclBoundaryFile(sf.fileName)); return this.declBoundaryMemo; } + /** Tracked classes' bag initializers over a typed `this` + * (class-dynamic-dispatch.ts recordBagInitializers). */ + readonly classBagInitializers = new Map(); constructor( readonly program: ts.Program, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e60a6600b..1140ced8c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3745,6 +3745,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** A class field's creation in its instance's property bag: adds the + * string key as a default data property when absent, answering false + * (nothing changed) when the bag needs the general defineProperty path. */ + "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, "dyn.objCreate": { argTypes: [DYN], result: DYN }, // Names a compiled class's prototype object (args[0]) after the class diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 907486fea..d26fb9d5a 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16110,3 +16110,18 @@ void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { } scr_dyn_release(old); } + +/* A compiled class's field creation in its instance's property bag (the + * compiler's trackClassFieldCreation): an ordinary extensible bag gains + * `key` as a writable, enumerable, configurable data property when it + * does not have it yet. Answers false, changing nothing, for any other bag, + * whose creation takes the general defineProperty path. Calls only + * exported functions. */ +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { + if (!bag || bag->kind != SCR_DYN_OBJ || bag->non_extensible || bag->v.obj.source_access) + return false; + if (scr_dyn_obj_get(bag, key->data, key->len)) return true; + scr_dyn_obj_set(bag, key->data, key->len, scr_dyn_retain(scr_dyn_undefined())); + return true; +} From 756a93a8f20a6da11b1d1af8f083e7d8e9ae57c2 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:18:03 -0700 Subject: [PATCH 137/169] Exit the untyped class tables early for values that are not class capsules Co-Authored-By: Claude Opus 5.5 --- .../backend/llvm/checked-value-lifetimes.ts | 2 + .../compiler/src/backend/llvm/expr-dynamic.ts | 5 + .../lowering/class-dynamic-dispatch.ts | 112 +++++++++++++++++- packages/compiler/src/ir/ir.ts | 6 +- packages/runtime/src/scr_json.c | 9 ++ 5 files changed, 130 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts index 5f8119079..4169e86d8 100644 --- a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts @@ -5,6 +5,8 @@ export function preservesDynTest(test: string): boolean { switch (test) { case "truthy": + case "classCapsule": + case "funcKind": case "nullish": case "promise": case "buffer": diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 434435eae..07a538184 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -719,6 +719,10 @@ export function emitDynamicExpr( B.line( `${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`, ); + } else if (e.test === "classCapsule") { + host.declare(`declare zeroext i1 @scr_dyn_is_class_capsule(ptr)`); + test = B.tmp(); + B.line(`${test} = call zeroext i1 @scr_dyn_is_class_capsule(ptr ${d.name})`); } else if (e.test === "truthy") { host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`); test = B.tmp(); @@ -839,6 +843,7 @@ export function emitDynamicExpr( undefined: DYN_KIND.UNDEF, null: DYN_KIND.NULL, bytes: DYN_KIND.BYTES, + funcKind: DYN_KIND.FUNC, }; test = oneOf([kindOf[e.test]!]); } diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index b5b585989..eb0529e76 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -394,6 +394,38 @@ export function classInstanceOf( }; } +/** `value` is the parameter itself, possibly through js.fromDyn. */ +function returnsParam(value: IrExpr, param: string): boolean { + if (value.kind === "varRef") return value.localId === param; + return ( + value.kind === "libCall" && + value.fn === "js.fromDyn" && + value.args.length === 1 && + value.args[0]!.kind === "varRef" && + value.args[0]!.localId === param + ); +} + +/** The kind a class-table branch requires of `param`: a capsule-key test + * (alone or guarding a conjunction) needs a capsule, a class-key test a + * function value; null for any other condition. */ +function branchKind(cond: IrExpr, param: string): "classCapsule" | "funcKind" | null { + if ( + cond.kind === "libCall" && + (cond.fn === "dyn.typedRefIs" || cond.fn === "dyn.typedRefKeyIs" || cond.fn === "dyn.classIs") && + cond.args[0]?.kind === "varRef" && + cond.args[0].localId === param + ) { + if (cond.fn === "dyn.classIs") return "funcKind"; + const key = cond.args[1]; + return key?.kind === "strLit" && key.value.startsWith("object:") ? "classCapsule" : null; + } + if (cond.kind === "ternary" && cond.else_.kind === "boolLit" && cond.else_.value === false) + return branchKind(cond.cond, param); + if (cond.kind === "logical" && cond.op === "&&") return branchKind(cond.left, param); + return null; +} + const READ_PROPERTIES = "%dyn.class.readProperties"; const READ_PROPERTIES_OR_EMPTY = "%dyn.class.readPropertiesOrEmpty"; @@ -4698,6 +4730,7 @@ export class ClassDynamicDispatch { fn.name === READ_PROPERTIES_OR_EMPTY, ); } + if (lowerer) this.guardClassTables(lowerer); } /** Each tracked class's bag initializer over a typed receiver (`this`), @@ -4715,6 +4748,63 @@ export class ClassDynamicDispatch { } } + /** Fast exits for the generated class tables: every class branch of + * the property-bag, normalization and static-bag helpers tests the + * value's capsule key (or class key), so a value of another kind (a + * plain object, the common case in untyped code) skips straight to the + * table's fallthrough instead of testing every class in the program. */ + private guardClassTables(lowerer: Lowerer): void { + const names = new Set([ + "%dyn.class.properties", + READ_PROPERTIES, + READ_PROPERTIES_OR_EMPTY, + "%dyn.class.normalize", + "%dyn.class.staticProperties", + ]); + for (const fn of lowerer.liftedFns) { + if (!names.has(fn.name) || fn.body.length < 2) continue; + const param = fn.params[0]?.localId; + if (param === undefined) continue; + const last = fn.body[fn.body.length - 1]!; + if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) continue; + let test: "classCapsule" | "funcKind" | null = null; + let ok = true; + for (const stmt of fn.body.slice(0, -1)) { + if ( + stmt.kind === "assign" && + stmt.localId === param && + stmt.value.kind === "call" && + stmt.value.callee === "%dyn.class.normalize" + ) + continue; + const kind = stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; + if (kind === null || (test !== null && kind !== test)) { + ok = false; + break; + } + test = kind; + } + if (!ok || test === null) continue; + const first = fn.body[0]!; + if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) continue; + const loc = fn.loc; + fn.body.unshift({ + kind: "if", + cond: { + kind: "dynTest", + test, + negated: true, + value: varRef(param, DYN, loc), + type: BOOL, + loc, + }, + then: [structuredClone(last)], + else_: null, + loc, + }); + } + } + private normalizedDispatches(): IrFunction[] { return [ ...(this.propertyBag ? [this.propertyBag] : []), @@ -4786,10 +4876,17 @@ export class ClassDynamicDispatch { this.normalizationKey = key; const loc = this.normalization.loc; const value = varRef("value", DYN, loc); - this.normalization.body = entries.map(({ capsule, children }): IrStmt => { + // The checked receiver is unboxed once per capsule branch, not once + // per subclass test. + this.normalization.locals = this.normalization.locals.filter( + (local) => !local.id.startsWith("%normalized."), + ); + this.normalization.body = entries.map(({ capsule, children }, index): IrStmt => { const type: IrType = { kind: "object", className: capsule.def.name }; - const receiver: IrExpr = { kind: "dynCheck", value, type, loc }; - return { + const id = `%normalized.${index}`; + this.normalization!.locals.push({ id, name: id, type, mutable: false }); + const receiver: IrExpr = varRef(id, type, loc); + const branch: IrStmt = { kind: "if", cond: { kind: "libCall", @@ -4830,6 +4927,15 @@ export class ClassDynamicDispatch { else_: null, loc, }; + // The capsule test stays the branch condition; its body unboxes. + if (branch.kind === "if") + branch.then.unshift({ + kind: "varDecl", + localId: id, + init: { kind: "dynCheck", value, type, loc }, + loc, + }); + return branch; }); this.normalization.body.push({ kind: "return", value, loc }); } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 63286ff54..6d0760b81 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3445,7 +3445,11 @@ export type IrExpr = | "array" | "truthy" | "error" - | "function"; + | "function" + /** The value is a capsule of a compiled class instance. */ + | "classCapsule" + /** The value is a boxed function or class value (exact kind test). */ + | "funcKind"; bytesElem?: IrBytesElem; negated?: true; value: IrExpr; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d26fb9d5a..3e197d9e8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16125,3 +16125,12 @@ bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { scr_dyn_obj_set(bag, key->data, key->len, scr_dyn_retain(scr_dyn_undefined())); return true; } + +/* Whether a value is a capsule of a compiled class instance (its type key + * names an object type): the class tables' fast exit for every other value, + * including record and array capsules. Calls only libc. */ +bool scr_dyn_is_class_capsule(const ScrDyn *d); +bool scr_dyn_is_class_capsule(const ScrDyn *d) { + return d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.type_key_len > 7 && + memcmp(d->v.typed_ref.type_key, "object:", 7) == 0; +} From 604f647e30fc1a5dde3fa7c0fe124406a3e47eb0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 23:10:08 -0700 Subject: [PATCH 138/169] Update the normalization unit test for the hoisted receiver Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-dynamic-dispatch.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index b717452c2..1a21a6aa5 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -458,9 +458,11 @@ test("normalizes base capsules before dispatching each derived member once", () }); expect(dispatch.body.filter((statement) => statement.kind === "if")).toHaveLength(2); const normalize = liftedFns.find((helper) => helper.name === "%dyn.class.normalize")!; + // The capsule branch unboxes its receiver once, then tests each subclass. expect(normalize.body[0]).toMatchObject({ kind: "if", then: [ + { kind: "varDecl", init: { kind: "dynCheck", type: { kind: "object", className: "Base" } } }, { kind: "if", then: [ From da546c05b311fd5b81e5d5d79c52035e46951a1c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 23:51:04 -0700 Subject: [PATCH 139/169] Format the class-table exits Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-dynamic-dispatch.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index eb0529e76..5fe14a8d8 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -412,7 +412,9 @@ function returnsParam(value: IrExpr, param: string): boolean { function branchKind(cond: IrExpr, param: string): "classCapsule" | "funcKind" | null { if ( cond.kind === "libCall" && - (cond.fn === "dyn.typedRefIs" || cond.fn === "dyn.typedRefKeyIs" || cond.fn === "dyn.classIs") && + (cond.fn === "dyn.typedRefIs" || + cond.fn === "dyn.typedRefKeyIs" || + cond.fn === "dyn.classIs") && cond.args[0]?.kind === "varRef" && cond.args[0].localId === param ) { @@ -4766,7 +4768,8 @@ export class ClassDynamicDispatch { const param = fn.params[0]?.localId; if (param === undefined) continue; const last = fn.body[fn.body.length - 1]!; - if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) continue; + if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) + continue; let test: "classCapsule" | "funcKind" | null = null; let ok = true; for (const stmt of fn.body.slice(0, -1)) { @@ -4777,7 +4780,8 @@ export class ClassDynamicDispatch { stmt.value.callee === "%dyn.class.normalize" ) continue; - const kind = stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; + const kind = + stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; if (kind === null || (test !== null && kind !== test)) { ok = false; break; @@ -4786,7 +4790,8 @@ export class ClassDynamicDispatch { } if (!ok || test === null) continue; const first = fn.body[0]!; - if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) continue; + if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) + continue; const loc = fn.loc; fn.body.unshift({ kind: "if", From b7d187f889290a7549ef4aeadb6d86bf365e5d7d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:10:57 -0500 Subject: [PATCH 140/169] Keep inline caches for bag helpers with class-table exits --- packages/compiler/src/ir/js-ic.test.ts | 61 ++++++++++++++++++++++++++ packages/compiler/src/ir/js-ic.ts | 16 ++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index 1b566fd00..f71a1e9db 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -176,3 +176,64 @@ test("a function holds a site when it calls a classified helper or dispatches a expect(hasJsIcSite({ ...invoker, name: "%init.0" }, sites)).toBe(false); expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true); }); + +test("bag helpers that exit early for values no class branch matches are still the receiver", () => { + /** `if (!(v)) return `: the class tables' early exit. */ + const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({ + kind: "if", + cond: { + kind: "dynTest", + test, + ...(negated ? { negated: true as const } : {}), + value: ref("v"), + type: BOOL, + loc, + }, + then: [ret(value)], + else_: null, + loc, + }); + const guarded = (name: string, first: IrStmt): IrFunction => + helper(name, ["v"], [first, capsuleBranch("v"), ret(ref("v"))]); + const readBag = guarded("%dyn.class.readPropertiesOrEmpty", exit("classCapsule", ref("v"))); + const writeBag = guarded("%dyn.class.properties", exit("classCapsule", ref("v"))); + const staticBag = guarded("%dyn.class.staticProperties", exit("funcKind", ref("v"))); + const getter = getHelper( + "%dyn.class.property.0", + "x", + [], + call(staticBag.name, call(readBag.name, ref("p.0"))), + ); + const setter = setHelper("%dyn.class.property.1", "y"); + // An exit that returns another value, or a test that is not negated, + // makes the bag differ from the receiver. + const otherExit = guarded( + "%dyn.class.other", + exit("classCapsule", { kind: "dynObjLit", type: DYN, loc } as IrExpr), + ); + const positive = guarded("%dyn.class.positive", exit("classCapsule", ref("v"), false)); + const otherGetter = getHelper("%dyn.class.property.2", "x", [], call(otherExit.name, ref("p.0"))); + const positiveGetter = getHelper( + "%dyn.class.property.3", + "x", + [], + call(positive.name, ref("p.0")), + ); + const sites = new JsIcSites({ + functions: [ + readBag, + writeBag, + staticBag, + otherExit, + positive, + getter, + setter, + otherGetter, + positiveGetter, + ], + }); + 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(); + 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 316b3c958..752316db3 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -170,6 +170,18 @@ export class JsIcSites { ); } + /** An early exit for values a class table never matches: `if (!(value)) return `, with no else. A plain object takes it + * and gets itself back. */ + private kindExit(s: IrStmt, param: string): boolean { + if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false; + const { cond } = s; + if (cond.kind !== "dynTest" || cond.negated !== true || !isVar(cond.value, param)) return false; + if (cond.test !== "classCapsule" && cond.test !== "funcKind") return false; + const ret = s.then[0]!; + return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); + } + /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -190,7 +202,9 @@ export class JsIcSites { const param = fn.params[0]!.localId; const last = fn.body[fn.body.length - 1]; identity = - fn.body.slice(0, -1).every((s) => this.capsuleBranch(s, param)) && + fn.body + .slice(0, -1) + .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); From 358d2df536fba442ce134015f5dfd2c510902b24 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:23:33 -0500 Subject: [PATCH 141/169] Keep package results that pass their argument through out of the declaration boundary A package function declared to return its own type parameter, which also types one of its parameters ((obj: T) => T), hands back the argument or something standing in for it, and the package may track that object's identity. Effect's Equal.byReference returns a proxy it keeps in a WeakSet. The declaration-typed boundary cast such a result to the argument's type and built a separate native record, so Equal.equals(a, b) on two by-reference objects compared them by value and printed true where Node prints false. The result's type comes from the caller's argument, not from data the package declares, so the projection no longer casts these calls. The package re-export coverage test now expects three dynamic statements: since the boundary, readRows's declared number result keeps its type, so count - 1 in 7004-package-declared-type-reexports is native arithmetic. --- .../compiler/src/frontend/decl-projection.ts | 82 +++++++++++++++++-- .../npm/cases/decl-pass-through/main.ts.json | 6 ++ .../npm/cases/decl-pass-through/main.ts | 16 ++++ .../node_modules/ref-marks/lib/index.d.ts | 8 ++ .../node_modules/ref-marks/lib/index.js | 18 ++++ .../node_modules/ref-marks/package.json | 1 + .../npm/cases/decl-pass-through/package.json | 1 + tests/harness/coverage.test.ts | 7 +- 8 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json create mode 100644 tests/fixtures/npm/cases/decl-pass-through/main.ts create mode 100644 tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts create mode 100644 tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js create mode 100644 tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json create mode 100644 tests/fixtures/npm/cases/decl-pass-through/package.json diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 31d32834e..f2340e984 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -99,6 +99,70 @@ function declaredInPackage(symbol: ts.Symbol | undefined): boolean { return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d))); } +/** The parts of a call signature's declaration that pass-through detection + * reads, for the declaration kinds a resolved call signature has. */ +function signatureParts(node: ts.Node | undefined): { + typeParameters: readonly ts.TypeParameterDeclaration[]; + parameters: readonly ts.ParameterDeclaration[]; + type: ts.TypeNode | undefined; +} | null { + if (node === undefined) return null; + if (ts.isFunctionTypeNode(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isFunctionDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isCallSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + return null; +} + +/** A signature that returns one of its own type parameters which also types + * a parameter (`(obj: T) => T`) hands back the argument + * or something standing in for it, a proxy or a marked object, whose + * identity the package may track (Effect's `Equal.byReference`). Its result + * type comes from the caller's argument, not from data the package + * declares, so the boundary leaves it alone: converting it would build a + * separate native value. */ +function passesArgumentThrough(declaration: ts.Node | undefined): boolean { + const sig = signatureParts(declaration); + if (sig === null || sig.type === undefined || !ts.isTypeReferenceNode(sig.type)) return false; + const ret = sig.type; + if (!ts.isIdentifier(ret.typeName) || ret.typeArguments !== undefined) return false; + const name = ret.typeName.text; + if (!sig.typeParameters.some((tp) => tp.name.text === name)) return false; + return sig.parameters.some( + (param) => + param.type !== undefined && + ts.isTypeReferenceNode(param.type) && + ts.isIdentifier(param.type.typeName) && + param.type.typeName.text === name, + ); +} + function declaredInUserFile(symbol: ts.Symbol | undefined): boolean { if (symbol === undefined) return false; const decls = symbol.declarations; @@ -511,16 +575,16 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | if (ts.isCallExpression(d) || ts.isNewExpression(d)) { const callee = d.expression; const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee; - produced = declaredInPackage(declSymbol(target)); - if (!produced) { - try { - const sig = declChecker.getResolvedSignature(d); - const decl = sig?.declaration; - produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)); - } catch { - produced = false; - } + let decl: ts.Node | undefined; + try { + decl = declChecker.getResolvedSignature(d)?.declaration; + } catch { + decl = undefined; } + produced = + (declaredInPackage(declSymbol(target)) || + (decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)))) && + !passesArgumentThrough(decl); } else if (ts.isTaggedTemplateExpression(d)) { produced = declaredInPackage(declSymbol(d.tag)); } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json new file mode 100644 index 000000000..1fd5083da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/decl-pass-through/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/decl-pass-through/main.ts b/tests/fixtures/npm/cases/decl-pass-through/main.ts new file mode 100644 index 000000000..a8168edc4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/main.ts @@ -0,0 +1,16 @@ +// Package functions declared to return their argument's own type +// (`(obj: T) => T`) hand back the argument, or a proxy +// standing in for it, and the package tracks that object's identity: those +// results stay the package's objects. Reads of data the declarations +// describe (`point(): Point`) are still typed. +import { byReference, equals, mark, point } from "ref-marks"; + +const a = byReference({ n: 42 }); +const b = byReference({ n: 42 }); +console.log(equals(a, a), equals(a, b), a.n + b.n); +const m = mark({ n: 1 }); +const m2 = mark({ n: 1 }); +console.log(equals(m, m), equals(m, m2)); +const p = point(1, 2); +const q = point(1, 2); +console.log(equals(p, q), p.x + q.y); diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts new file mode 100644 index 000000000..aa0df42f4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts @@ -0,0 +1,8 @@ +export interface Point { + x: number; + y: number; +} +export declare function byReference(obj: T): T; +export declare const mark: (obj: T) => T; +export declare function equals(a: unknown, b: unknown): boolean; +export declare function point(x: number, y: number): Point; diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js new file mode 100644 index 000000000..796efb19b --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js @@ -0,0 +1,18 @@ +const marked = new WeakSet(); +export function byReference(obj) { + const proxy = new Proxy(obj, {}); + marked.add(proxy); + return proxy; +} +export function mark(obj) { + marked.add(obj); + return obj; +} +export function equals(a, b) { + if (a === b) return true; + if (marked.has(a) || marked.has(b)) return false; + return JSON.stringify(a) === JSON.stringify(b); +} +export function point(x, y) { + return { x, y }; +} diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json new file mode 100644 index 000000000..5ae95f315 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json @@ -0,0 +1 @@ +{ "name": "ref-marks", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/fixtures/npm/cases/decl-pass-through/package.json b/tests/fixtures/npm/cases/decl-pass-through/package.json new file mode 100644 index 000000000..4895e9aec --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/package.json @@ -0,0 +1 @@ +{ "name": "decl-pass-through-case", "private": true, "type": "module" } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 3ed562aea..e1831c609 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -561,13 +561,14 @@ test("types only a package's .d.ts declares stay static through re-exports and . // shape-kinds re-exports types its .d.ts entry imports; row-module's // runtime entry is an .mjs file. Without their declared types the // program's uses of Form, Kind and Row run in the dynamic tier (12 - // statements); with them only the packages' own JavaScript and one - // arithmetic on a JavaScript return value do (4). + // statements); with them only the packages' own JavaScript does (3). + // readRows's result keeps its declared number type, so `count - 1` is + // native arithmetic too. const { coverage } = analyze( join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"), ); expect(coverage.diagnostics).toEqual([]); - expect(coverage.stats.statementsDynamic).toBe(4); + expect(coverage.stats.statementsDynamic).toBe(3); }); test("any-typed checked-dynamic operators compile natively", () => { From 1edb2fe92d954ec6ef5fac0f4eb3d7163fc5a253 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 00:29:27 -0700 Subject: [PATCH 142/169] Keep fiber stacks across short event-loop sleeps Finished fiber stacks went back to a spare pool that the event loop unmapped whenever it was about to sleep. A server's loop sleeps between nearly every pair of requests, so each request that awaited mapped, faulted in and unmapped a fresh 512 KiB stack: on a Hono server mmap, munmap and the page faults were about a fifth of its CPU time. Spares now outlive short sleeps. At most once a second a sleep unmaps the spares that no fiber took since the previous trim, and a sleep longer than two seconds unmaps them all, so an idle program still returns the memory. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_async.c | 45 ++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..6afd174b7 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -72,15 +72,26 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we #if !defined(_WIN32) && !defined(__wasi__) -/* Keep stacks out of malloc's size classes. Reuse a bounded number during - * a burst of promise/generator work, then unmap the spares before the loop - * sleeps. This avoids both heap fragmentation and per-call mmap overhead. - * The sanitizer lane unmaps every finished stack immediately. */ +/* Keep stacks out of malloc's size classes. Reuse a bounded number of + * finished stacks, which avoids both heap fragmentation and per-call mmap + * overhead. A server's loop sleeps between nearly every pair of requests, + * so unmapping the spares at each sleep would map, fault in and unmap a + * stack for every request that awaits. Spares therefore outlive short + * sleeps: at most once per SCR_FIBER_TRIM_MS, a sleep unmaps the spares + * that no fiber took since the previous trim (the pool's low-water mark), + * and a sleep longer than twice that unmaps every spare, so an idle + * program gives the memory back as before. The sanitizer lane unmaps + * every finished stack immediately. */ #ifndef SCR_ASAN_FIBERS #define SCR_FIBER_SPARES 64 +#define SCR_FIBER_TRIM_MS 1000.0 static SCR_TL void *scr_fiber_spares[SCR_FIBER_SPARES]; static SCR_TL size_t scr_fiber_nspares; static SCR_TL bool scr_fiber_cleanup_registered; +/* The fewest spares the pool held since the last trim: the spares below + * this index went unused for the whole window. */ +static SCR_TL size_t scr_fiber_spares_low; +static SCR_TL double scr_fiber_trimmed_at; #endif static void scr_fiber_stacks_clear(void) { @@ -89,6 +100,29 @@ static void scr_fiber_stacks_clear(void) { (void)munmap(scr_fiber_spares[--scr_fiber_nspares], SCR_FIBER_STACK); scr_fiber_spares[scr_fiber_nspares] = NULL; } + scr_fiber_spares_low = 0; +#endif +} + +/* The loop is about to sleep for `wait` ms (see above). */ +static void scr_fiber_stacks_trim(double now, double wait) { +#ifndef SCR_ASAN_FIBERS + if (wait > 2 * SCR_FIBER_TRIM_MS) { + scr_fiber_stacks_clear(); + scr_fiber_trimmed_at = now; + return; + } + if (now - scr_fiber_trimmed_at < SCR_FIBER_TRIM_MS) return; + size_t unused = scr_fiber_spares_low; + for (size_t i = 0; i < unused; i++) (void)munmap(scr_fiber_spares[i], SCR_FIBER_STACK); + scr_fiber_nspares -= unused; + memmove(scr_fiber_spares, scr_fiber_spares + unused, scr_fiber_nspares * sizeof *scr_fiber_spares); + memset(scr_fiber_spares + scr_fiber_nspares, 0, unused * sizeof *scr_fiber_spares); + scr_fiber_spares_low = scr_fiber_nspares; + scr_fiber_trimmed_at = now; +#else + (void)now; + (void)wait; #endif } @@ -97,6 +131,7 @@ static void *scr_fiber_stack_new(void) { if (scr_fiber_nspares > 0) { void *stack = scr_fiber_spares[--scr_fiber_nspares]; scr_fiber_spares[scr_fiber_nspares] = NULL; + if (scr_fiber_nspares < scr_fiber_spares_low) scr_fiber_spares_low = scr_fiber_nspares; return stack; } if (!scr_fiber_cleanup_registered) { @@ -4599,7 +4634,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { /* An idle moment: a full pass the death rule deferred runs now. */ if (due > now && scr_cyc_collect_idle()) now = scr_now_ms(); #if !defined(_WIN32) && !defined(__wasi__) - if (due > now) scr_fiber_stacks_clear(); + if (due > now) scr_fiber_stacks_trim(now, due - now); #endif #ifdef SCR_WORKERS if (due > now) scr_sa_loop_waiting(true); From b8d442e3ae9a2a14443e22f2226caef8492fd899 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:26 -0700 Subject: [PATCH 143/169] Run match and search with untyped patterns s.match(p) and s.search(p) with an untyped pattern now choose at run time, as Node does: a RegExp is used as is (with its lastIndex state), any other value builds new RegExp(value). The checked-dynamic receiver form previously threw 'method calls like path.match on a string are not supported yet' (hono's RegExpRouter matcher). Co-Authored-By: Claude Opus 5.5 --- .../lowering/containers/string-and-regexp.ts | 59 +++++++++++++++++-- .../corpus/7732-untyped-match-pattern.js.json | 6 ++ tests/corpus/7732-untyped-match-pattern.js | 15 +++++ .../7732-untyped-match-pattern.js.json | 1 + 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json create mode 100644 tests/corpus/7732-untyped-match-pattern.js create mode 100644 tests/harness/corpus-costs/7732-untyped-match-pattern.js.json diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 1947f0d75..d55f71ff0 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -16,6 +16,7 @@ import { type IrExpr, type IrStmt, type IrType, + REGEX, STRING, type SrcLoc, UNDEFINED_T, @@ -920,12 +921,15 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -983,10 +987,13 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence const receiver = lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1936,6 +1943,46 @@ function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean ); } +/** True when a `match` or `search` pattern is checker-untyped (`any` or + * `unknown`), so its runtime value decides the protocol. */ +function isCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + if (ts.isSpreadElement(node)) return false; + return (lowerer.typeOf(node).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +/** An untyped `match` or `search` pattern as a RegExp, chosen at run time as + * Node does: a RegExp is used as is (its lastIndex state included), and any + * other value builds `new RegExp(value)` (undefined is the empty pattern, + * other values convert with ToString). */ +function lowerCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const loc = locOf(node); + const value = lowerer.lowerExpr(node); + if (value.type.kind === "regex") return value; + const checked = lowerer.coerceInto(node, value, DYN); + const local = lowerer.declareHiddenLocal("%matchPattern", DYN); + const ref = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: checked, loc }], + result: { + kind: "ternary", + cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [ref], type: BOOL, loc }, + then: lowerer.coerceInto(node, ref, REGEX), + else_: { + kind: "libCall", + fn: "regex.newChecked", + args: [ref, dynUndefinedExpr(loc)], + type: REGEX, + loc, + }, + type: REGEX, + loc, + }, + type: REGEX, + loc, + }; +} + /** The pattern of `match` or `search` as a RegExp: an undefined pattern is * `new RegExp(undefined)`, the empty pattern, as in Node. */ function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..f6355a2ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7732-untyped-match-pattern.js" + ], + "diags": [] +} diff --git a/tests/corpus/7732-untyped-match-pattern.js b/tests/corpus/7732-untyped-match-pattern.js new file mode 100644 index 000000000..2079bb8ef --- /dev/null +++ b/tests/corpus/7732-untyped-match-pattern.js @@ -0,0 +1,15 @@ +// String.prototype.match and search with an untyped pattern: a RegExp is used as is, +// any other value builds new RegExp(value), as hono's matcher does. +const matchers = { GET: [/^\/a\/([^/]+)$()|^\/b$()/, [0, 1], {}] }; +function match(method, path) { + const matcher = matchers[method] || matchers["ALL"]; + const m = path.match(matcher[0]); + if (!m) return null; + return [m.indexOf("", 1), m.length, m[1], m.index]; +} +console.log(JSON.stringify(match("GET", "/a/xy"))); +console.log(JSON.stringify(match("GET", "/b"))); +console.log(JSON.stringify(match("GET", "/c"))); +const g = { re: /o/g, s: "o.", n: null }; +function m2(s, k) { return s.match(g[k]); } +console.log(JSON.stringify(m2("foo boo", "re")), JSON.stringify(m2("go.o", "s")), JSON.stringify(m2("null", "n"))); diff --git a/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..2de3f6b94 --- /dev/null +++ b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3300 } From be6e63735a3a2fe81616ec80b331fc7d62d167c5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:25 -0700 Subject: [PATCH 144/169] Index strings through checker-any receivers An implicit-any package function specialized for a string argument lowered base?.[0] to the element-access refusal: the checker types the receiver any, so only the lowered string reached the string-index path. Hono's mergePath failed at startup. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/expressions/element-access.ts | 10 ++++++++-- .../corpus/7731-npm-any-string-index/main.ts.json | 6 ++++++ tests/corpus/7731-npm-any-string-index/main.ts | 9 +++++++++ .../node_modules/merge-path/index.d.ts | 2 ++ .../node_modules/merge-path/index.js | 10 ++++++++++ .../node_modules/merge-path/package.json | 1 + .../node_modules/merge-path/url.js | 10 ++++++++++ .../7731-npm-any-string-index/main.ts.json | 1 + 8 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json create mode 100644 tests/corpus/7731-npm-any-string-index/main.ts create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js create mode 100644 tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0be9be81a..0ea94ec7a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -566,8 +566,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } if (receiverIr?.kind !== "array") { + const receiver = tryLowerExpression(lowerer, expr.expression); { - const receiver = tryLowerExpression(lowerer, expr.expression); if ( receiver?.type.kind === "dyn" || (receiver && @@ -618,7 +618,13 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } - if (receiverIr?.kind === "string") { + // A checker-`any` receiver that lowers to a string (an implicit-any + // package parameter specialized for a string argument) indexes the + // string the same way. + if ( + receiverIr?.kind === "string" || + (receiverIr === null && receiver?.type.kind === "string") + ) { // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..78fc40580 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7731-npm-any-string-index/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7731-npm-any-string-index/main.ts b/tests/corpus/7731-npm-any-string-index/main.ts new file mode 100644 index 000000000..21e7d1c10 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/main.ts @@ -0,0 +1,9 @@ +// An implicit-any package function specialized for a string argument indexes +// the string (`base?.[0]`), as hono's mergePath does. +import { App, mergePath } from "merge-path"; +console.log(new App().request("x")); +console.log(new App().request(new URL("http://h/p"))); +console.log(new App().basePath("/api").basePath("v1/")._basePath); +console.log(mergePath("/api/", "/"), mergePath("a", "b", "/c")); +console.log(new App().basePath("").basePath("").request("")); +console.log(mergePath("", "x"), mergePath("/", "/"), mergePath("//", "y/")); diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts new file mode 100644 index 000000000..aa5142307 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts @@ -0,0 +1,2 @@ +export declare const mergePath: (...paths: string[]) => string; +export declare class App { _basePath: string; request: (input: string | URL) => string; basePath(path: string): App; } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js new file mode 100644 index 000000000..6a5c7c86b --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js @@ -0,0 +1,10 @@ +import { mergePath } from "./url.js"; +class App { + _basePath = "/"; + request = (input) => { + input = input.toString(); + return mergePath("/", input); + }; + basePath(path) { const s = new App(); s._basePath = mergePath(this._basePath, path); return s; } +} +export { App, mergePath }; diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json new file mode 100644 index 000000000..ddc3db4ef --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json @@ -0,0 +1 @@ +{ "name": "merge-path", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js new file mode 100644 index 000000000..e27469714 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js @@ -0,0 +1,10 @@ +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.[base.length - 1] === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +export { mergePath }; diff --git a/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..9fe9ad101 --- /dev/null +++ b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6200, "san": 5800 } From fdcee191da193bbaed9f78d36aad1b70d29bb96a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:27 -0700 Subject: [PATCH 145/169] Give rebuilt property-bag stores their prototype Worklist passes that transform every function (the class-construction dispatch rewriting dyn.classBasePrototype) rebuild statement objects, so a recorded lazy bag store could be a detached copy. When the class gained prototype data later, only the copy was upgraded and the live store kept creating a bare {} bag: inherited methods read as undefined through the property bag (hono's SmartRouter.match once zod was in the program). Upgrades now also reach the stores as they stand in the generated bodies. Co-Authored-By: Claude Opus 5.5 --- .../lowering/class-dynamic-dispatch.ts | 28 +++++++++- .../7733-property-bag-late-prototype.js.json | 6 +++ .../7733-property-bag-late-prototype.js | 52 +++++++++++++++++++ .../7733-property-bag-late-prototype.js.json | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json create mode 100644 tests/corpus/7733-property-bag-late-prototype.js create mode 100644 tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index e394718d1..6548d33d2 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1229,6 +1229,7 @@ export class ClassDynamicDispatch { } } } + let live: Map[]> | null = null; for (const plan of this.propertyReceivers(lowerer, false, false)) { const info = plan.info; const className = info.def.name; @@ -1309,7 +1310,11 @@ export class ClassDynamicDispatch { ); const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => this.bagPrototype(lowerer, current, receiver, loc); - for (const existingInit of this.bagInitializers.get(className) ?? []) { + live ??= this.liveBagInitializers(); + for (const existingInit of [ + ...(this.bagInitializers.get(className) ?? []), + ...(live.get(className) ?? []), + ]) { if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) { const prototype = prototypeFor(info, existingInit.obj, existingInit.loc); if (prototype) { @@ -4440,6 +4445,27 @@ export class ClassDynamicDispatch { * A class with constructible subclasses that have prototype data selects * it through its `%class.bagPrototype` helper; any other class reads its * own prototype data. */ + /** The lazy bag stores as they stand in the generated bodies, by class. + * Worklist passes that transform every function rebuild statement + * objects, so a recorded initializer can be a detached copy; a store that + * gains its prototype must be updated where it runs. */ + private liveBagInitializers(): Map[]> { + const live = new Map[]>(); + const add = (stmt: IrStmt | undefined): void => { + if (stmt?.kind !== "if") return; + const store = stmt.then[0]; + if (store?.kind !== "fieldSet" || store.field !== PROPERTY_BAG) return; + const list = live.get(store.className); + if (list) list.push(store); + else live.set(store.className, [store]); + }; + // The complete dispatch guards each class's lazy store by its receiver. + for (const stmt of this.propertyBag?.body ?? []) if (stmt.kind === "if") add(stmt.then[0]); + for (const helper of this.typedPropertyBags.values()) add(helper.body[0]); + for (const helper of this.exactPropertyBags.values()) add(helper.body[0]); + return live; + } + private bagPrototype( lowerer: Lowerer, current: ClassInfo, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..2b9778666 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7733-property-bag-late-prototype.js" + ], + "diags": [] +} diff --git a/tests/corpus/7733-property-bag-late-prototype.js b/tests/corpus/7733-property-bag-late-prototype.js new file mode 100644 index 000000000..f88212b62 --- /dev/null +++ b/tests/corpus/7733-property-bag-late-prototype.js @@ -0,0 +1,52 @@ +// A class whose property bag is first created without prototype data, while a local +// class with an untyped base rewrites the shared bag dispatch: the store gains the +// prototype later, so inherited methods stay readable (hono's SmartRouter with zod). +function $constructor(name, initializer, params) { + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + inst.def = def; + initializer(inst, def); + return inst; + } + Object.defineProperty(_, "name", { value: name }); + return _; +} +class Smart { + name = "S"; + #inner = []; + constructor(init) { this.#inner = init.routers; } + add(x) { this.#inner.push(x); } + match(m) { + const r = this.#inner[0]; + this.match = r.match.bind(r); + this.name = `S + ${r.name}`; + return "first " + m; + } +} +class Inner { name = "Inner"; match(m) { return "inner " + m; } } +class App { + router; + constructor(options = {}) { + const { strict, ...rest } = options; + Object.assign(this, rest); + this.router = options.router ?? new Smart({ routers: [new Inner()] }); + } + dispatch(m) { return this.router.match(m); } +} +const app = new App(); +console.log(Object.keys(app.router).join(",")); +const $Err = $constructor("$Err", (inst, def) => { inst.issues = def.issues; }, { Parent: Error }); +const e = new $Err({ issues: [1] }); +console.log(Object.keys(e).join(","), e.issues.length); +const run = [(a) => a.dispatch("a"), (a) => a.dispatch("b")]; +for (const f of run) console.log(f(app), app.router.name); +const holder = JSON.parse("{}"); +holder.r = new Smart({ routers: [new Inner()] }); +console.log(typeof holder.r.match, "match" in holder.r, Object.keys(holder.r).join(",")); +console.log(holder.r.match("c"), holder.r.match("d"), holder.r.name); +const scratch = JSON.parse("{\"name\":\"x\",\"other\":1}"); +delete scratch.name; +console.log(Object.keys(scratch).join(",")); diff --git a/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..706d1a2a5 --- /dev/null +++ b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 3900 } From e269262d7395019f0c47506f031914fcb7ef0d5d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:29 -0700 Subject: [PATCH 146/169] Iterate untyped Headers values entries(), keys() and values() on a checked-dynamic Headers value threw 'is not a function'. They now return live native iterators over the sorted header list, re-read on every step with set-cookie values one by one, as Node's Headers iterator does (hono's Context copies response headers this way). Co-Authored-By: Claude Opus 5.5 --- .../7735-untyped-headers-iterators.js.json | 6 ++++ packages/runtime/src/scr_fetch.c | 36 ++++++++++++++++++- .../corpus/7735-untyped-headers-iterators.js | 23 ++++++++++++ .../7735-untyped-headers-iterators.js.json | 1 + 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json create mode 100644 tests/corpus/7735-untyped-headers-iterators.js create mode 100644 tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..7ca5d34da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7735-untyped-headers-iterators.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..7cc2beef4 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -2491,11 +2491,44 @@ static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, return found; } +/* One step of a live Headers iterator (entries, keys or values): the + * `*index`th entry of the current sorted list, as Node's iterator re-reads + * the list on every step. The result is { value, done }. */ +static ScrDyn *sf_headers_step(void *ptr, double *index, unsigned selection) { + ScrStr *name = NULL; + ScrStr *value = NULL; + bool found = sf_headers_sorted_entry(ptr, (size_t)*index, &name, &value); + ScrDyn *item; + if (found) { + (*index)++; + if (selection == 2) { + item = scr_dyn_new_arr(); + scr_dyn_arr_push(item, scr_dyn_new_str(name)); + scr_dyn_arr_push(item, scr_dyn_new_str(value)); + } else { + item = scr_dyn_new_str(selection == 1 ? name : value); + } + scr_str_release(name); + scr_str_release(value); + } else { + item = scr_dyn_retain(scr_dyn_undefined()); + } + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, item); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(!found)); + return result; +} + static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfHeaders *h = ptr; + if (strcmp(method, "entries") == 0 || strcmp(method, "keys") == 0 || + strcmp(method, "values") == 0) { + return scr_dyn_native_handle_iterator( + self, strcmp(method, "entries") == 0 ? 2 : strcmp(method, "keys") == 0 ? 1 : 0); + } if (strcmp(method, "get") == 0 || strcmp(method, "has") == 0) { if (argc < 1) { sf_type_error("Headers.get/has requires a header name"); @@ -5487,7 +5520,8 @@ static const ScrDynHandleOps sf_response_ops = { &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, - &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL}; + &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, + &sf_headers_step}; static void sf_teardown(void) { while (sf_live) { diff --git a/tests/corpus/7735-untyped-headers-iterators.js b/tests/corpus/7735-untyped-headers-iterators.js new file mode 100644 index 000000000..40937fe5c --- /dev/null +++ b/tests/corpus/7735-untyped-headers-iterators.js @@ -0,0 +1,23 @@ +// Headers entries/keys/values on an untyped Headers value: live iterators over the +// sorted list, with set-cookie values one by one, as hono's Context copies headers. +const holder = JSON.parse("{}"); +holder.res = new Response("body", { headers: { "x-b": "2", "X-A": "1", "set-cookie": "s=1" } }); +holder.res.headers.append("set-cookie", "t=2"); +const headers = holder.res.headers; +for (const [k, v] of headers.entries()) console.log("entry", k, v); +console.log(JSON.stringify([...headers.keys()]), JSON.stringify([...headers.values()])); +const it = headers.entries(); +console.log(JSON.stringify(it.next())); +headers.append("a-first", "0"); +headers.append("z-last", "9"); +console.log(JSON.stringify(it.next()), JSON.stringify(it.next())); +const rest = []; +for (const entry of it) rest.push(entry.join("=")); +console.log(rest.join(" "), JSON.stringify(it.next())); +const copy = JSON.parse("{}"); +copy.res = new Response(null, { status: 204 }); +for (const [k, v] of headers.entries()) { + if (k === "set-cookie") continue; + copy.res.headers.set(k, v); +} +console.log(JSON.stringify([...copy.res.headers.entries()])); diff --git a/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..8836ebdf7 --- /dev/null +++ b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4000 } From b1214f76070fc34a45bb65f6c73ee13f41fb7f66 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:28 -0700 Subject: [PATCH 147/169] Test catch bindings against class bindings e instanceof E where e is a catch binding and E a binding holding a class expression (var E = class extends Error {}) refused with "'instanceof' right-hand sides other than classes declared in the program". The binding holds exactly that class, so the test is the class's caught test (hono's SmartRouter fallback on UnsupportedPathError). Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/relational.ts | 90 ++++++++++++------- ...34-caught-instanceof-class-binding.js.json | 6 ++ .../7734-caught-instanceof-class-binding.js | 22 +++++ ...34-caught-instanceof-class-binding.js.json | 1 + 4 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json create mode 100644 tests/corpus/7734-caught-instanceof-class-binding.js create mode 100644 tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index b731e4912..9f06cc377 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -400,7 +400,24 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // (itself — no descendants can flow into the slot), so the answer // folds statically exactly like the named-target folds below. const rhsClassval = storedClassValueType(lowerer, expr.right); - if (rhsClassval?.kind === "classval" && !lowerer.caughtLocalOf(expr.left)) { + // A catch binding against a class held by a binding (`var E = class + // extends Error {}`): the binding holds exactly that class, so the test + // is the class's caught test, after the right operand's read. + const caught = lowerer.caughtLocalOf(expr.left); + const caughtTarget = + rhsClassval?.kind === "classval" && caught + ? lowerer.classes.get(rhsClassval.className) + : undefined; + if (caught && caughtTarget && !caughtTarget.localClass && !caughtTarget.def.runtime) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.right), loc }], + result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + type: BOOL, + loc, + }; + } + if (rhsClassval?.kind === "classval" && !caught) { const targetInfo = lowerer.classes.get(rhsClassval.className); if (!targetInfo) { // The type world names a class the lowering never registered @@ -551,39 +568,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // narrowing then types the branches; reads bridge through caughtRead. const caughtLocal = lowerer.caughtLocalOf(expr.left); if (caughtLocal) { - const caughtInstanceOf = (info: ClassInfo): IrExpr => { - if (!lowerer.inHierarchy(info)) { - const value = varRef(caughtLocal.id, CAUGHT, loc); - return { - kind: "ternary", - cond: { - kind: "caughtTest", - value, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }, - then: { kind: "boolLit", value: true, type: BOOL, loc }, - else_: classInstanceOf( - lowerer, - { kind: "caughtToDyn", value, type: DYN, loc }, - info, - loc, - ), - type: BOOL, - loc, - }; - } - return { - kind: "caughtTest", - value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc }, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }; - }; + const caughtInstanceOf = (info: ClassInfo): IrExpr => + caughtClassInstanceOf(lowerer, caughtLocal.id, info, loc); let test = caughtInstanceOf(target); // A thrown checked-dynamic object whose prototype chain reaches // Error.prototype (an ES5 constructor function inheriting from it). @@ -978,6 +964,42 @@ function writableClassValueTest( }; } +/** A catch binding tested against a program class: the runtime test against + * the class's preorder interval (false for non-hierarchy-object payloads). */ +function caughtClassInstanceOf( + lowerer: Lowerer, + caughtId: string, + info: ClassInfo, + loc: SrcLoc, +): IrExpr { + if (!lowerer.inHierarchy(info)) { + const value = varRef(caughtId, CAUGHT, loc); + return { + kind: "ternary", + cond: { + kind: "caughtTest", + value, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }, + then: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: classInstanceOf(lowerer, { kind: "caughtToDyn", value, type: DYN, loc }, info, loc), + type: BOOL, + loc, + }; + } + return { + kind: "caughtTest", + value: { kind: "varRef", localId: caughtId, type: CAUGHT, loc }, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }; +} + /** A statically decided `instanceof` still reads its class after the left * operand: a class with an initialization flag throws there before its * declaration ran. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..9ce0ed4f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7734-caught-instanceof-class-binding.js" + ], + "diags": [] +} diff --git a/tests/corpus/7734-caught-instanceof-class-binding.js b/tests/corpus/7734-caught-instanceof-class-binding.js new file mode 100644 index 000000000..55ee792ac --- /dev/null +++ b/tests/corpus/7734-caught-instanceof-class-binding.js @@ -0,0 +1,22 @@ +// A catch binding tested against a class held by a binding (var/const class expression), +// as hono's SmartRouter tests UnsupportedPathError. +var UnsupportedPathError = class extends Error {}; +class Strict { + add(p) { if (p.includes("*")) throw new UnsupportedPathError(); return p; } +} +class Loose { add(p) { return "loose " + p; } } +function pick(p) { + const routers = [new Strict(), new Loose()]; + for (const r of routers) { + try { + return r.add(p); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + } +} +console.log(pick("/a"), pick("/b/*")); +try { throw new Error("x"); } catch (e) { console.log(e instanceof UnsupportedPathError, e instanceof Error); } +const u = new UnsupportedPathError("m"); +console.log(u instanceof UnsupportedPathError, u.message, u.name, u instanceof Error); diff --git a/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..71aa92bd3 --- /dev/null +++ b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 3600 } From a474ff7e8d15ea44f608885fdd7a6afce64e451d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:43:57 -0500 Subject: [PATCH 148/169] Check for Error accessors after definitions over object-typed targets The reflection gate turned on only for an untyped or Error-shaped defineProperty target, so a helper taking `target: object` (or `{}`, `Object`, a weak type, or a type parameter) installed a message or name accessor that typed Error reads then skipped, returning the old slot value. The gate now turns on for any target an Error may flow into: an untyped or generic type, a type Error is assignable to, or an Error-shaped type, alone or in a union or intersection. --- .../src/frontend/lowering/lower-calls.ts | 27 +++++-- .../7263-error-accessor-object-helper.ts.json | 6 ++ .../7263-error-accessor-object-helper.ts | 54 +++++++++++++ .../7263-error-accessor-object-helper.ts.json | 1 + .../harness/error-accessor-reflection.test.ts | 77 +++++++++++++++++++ 5 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json create mode 100644 tests/corpus/7263-error-accessor-object-helper.ts create mode 100644 tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json create mode 100644 tests/harness/error-accessor-reflection.test.ts diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 54bf9a30d..909c8f27a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -269,7 +269,7 @@ const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "cl /** `Object.defineProperty(target, ...)`, `Object.defineProperties`, * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` - * over an untyped or Error-shaped target can install an accessor over an + * over a target that may hold an Error can install an accessor over an * error's `name` or `message` (error-methods.ts). */ function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): void { const callee = expr.expression; @@ -282,15 +282,28 @@ function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): ? callee.expression : undefined; if (!target) return; - const type = lowerer.checker.getTypeAtLocation(target); - if ( - type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) || - (lowerer.checker.getPropertyOfType(type, "message") && - lowerer.checker.getPropertyOfType(type, "name")) - ) + const error = lowerer.checker.resolveName("Error", ts.SymbolFlags.Type, target, false); + const errorType = error ? lowerer.checker.getDeclaredTypeOfSymbol(error) : null; + if (mayHoldError(lowerer, lowerer.checker.getTypeAtLocation(target), errorType)) lowerer.errorAccessorReflection = locOf(expr); } +/** An untyped or generic type, a type Error is assignable to (`object`, + * `{}`, `Object`, `{ message?: string }`), or + * an Error-shaped type with both `name` and `message`, alone or in a + * union or intersection. */ +function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null): boolean { + if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Instantiable)) + return true; + if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) + return ts.constituentTypes(type).some((part) => mayHoldError(lowerer, part, errorType)); + return ( + (lowerer.checker.getPropertyOfType(type, "message") !== undefined && + lowerer.checker.getPropertyOfType(type, "name") !== undefined) || + (errorType !== null && lowerer.checker.isTypeAssignableTo(errorType, type)) + ); +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..8bf8f11da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7263-error-accessor-object-helper.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7263-error-accessor-object-helper.ts b/tests/corpus/7263-error-accessor-object-helper.ts new file mode 100644 index 000000000..8e8a4a532 --- /dev/null +++ b/tests/corpus/7263-error-accessor-object-helper.ts @@ -0,0 +1,54 @@ +// An accessor installed over an Error's own `message` and `name` by +// helpers whose target parameter is typed `object`, `{}` or generic: +// typed reads, writes and String() go through the accessors. +class ValidationError extends Error {} + +function defineLazyMessage(target: object, compute: () => string): void { + let cached: string | undefined; + Object.defineProperty(target, "message", { + get() { + cached ??= compute(); + return cached; + }, + set(value: string) { + cached = `set:${value}`; + }, + enumerable: true, + configurable: true, + }); +} + +function defineName(target: {}, name: string): void { + Object.defineProperty(target, "name", { get: () => name, configurable: true }); +} + +function defineFixedMessage(target: T, message: string): T { + Object.defineProperty(target, "message", { get: () => message, configurable: true }); + return target; +} + +let computed = 0; +const e = new ValidationError("orig"); +defineLazyMessage(e, () => { + computed++; + return "too small"; +}); +console.log(e.message, computed); +console.log(String(e), computed); +e.message = "typed"; +console.log(e.message, Object.keys(e).join()); + +const named = new ValidationError("bad input"); +defineName(named, "ZodError"); +console.log(named.name, String(named)); +const fixed = defineFixedMessage(new ValidationError("orig"), "fixed"); +console.log(fixed.message, String(fixed)); + +const plain = new Error("start"); +defineLazyMessage(plain, () => "computed"); +console.log(plain.message, `${plain}`); +try { + throw plain; +} catch (caught) { + console.log(caught instanceof Error && caught.message); +} diff --git a/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } diff --git a/tests/harness/error-accessor-reflection.test.ts b/tests/harness/error-accessor-reflection.test.ts new file mode 100644 index 000000000..5f50d8a66 --- /dev/null +++ b/tests/harness/error-accessor-reflection.test.ts @@ -0,0 +1,77 @@ +/* Reflection that can install an accessor over an error's `name` or + * `message`: a defineProperty call over a target that may hold an Error + * turns on the accessor checks of typed Error reads and writes + * (error-methods.ts), whatever the target parameter's declared type. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedIr(source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `error-accessor-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, "main.ts"); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, "main.ir"), + outDir, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +const program = ( + param: string, + define = "Object.defineProperty", +): string => `class E extends Error {} +function install${param} { + ${define}(target, "message", { get: () => "lazy", configurable: true }); +} +const e = new E("orig"); +install(e); +console.log(e.message); +`; + +const checksAccessors = async (source: string): Promise => + (await emittedIr(source)).includes("error.ownAccessor"); + +test.each([ + ["object", "(target: object)"], + ["{}", "(target: {})"], + ["Object", "(target: Object)"], + ["a weak type", "(target: { message?: string })"], + ["{ message: string }", "(target: { message: string })"], + ["a type parameter", "(target: T)"], + ["a constrained type parameter", "(target: T)"], + ["object | undefined", "(target: object | undefined)"], + ["Error", "(target: Error)"], + ["unknown", "(target: unknown)"], +])("a definition over %s checks for accessors", async (_, param) => { + expect(await checksAccessors(program(param))).toBe(true); +}); + +test("Reflect.defineProperty over object checks for accessors", async () => { + expect(await checksAccessors(program("(target: object)", "Reflect.defineProperty"))).toBe(true); +}); + +test("a definition over a type that cannot hold an Error leaves typed reads plain", async () => { + const source = `class E extends Error {} +class Box { + id = 0; +} +function install(target: Box) { + Object.defineProperty(target, "extra", { get: () => 1, configurable: true }); +} +install(new Box()); +const e = new E("orig"); +console.log(e.message); +`; + expect(await checksAccessors(source)).toBe(false); +}); From 2f903f0255d59851f17784918a5cbe61233656fa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:13:24 -0700 Subject: [PATCH 149/169] Read native handle methods as function values Methods of Web objects held in untyped code (Request, Response, Headers, ReadableStream, its reader, AbortSignal) answered member calls only by static name, so a property read such as hono's `raw[key]()` or `typeof req.json` saw undefined. A handle class now lists the methods its invoke answers; a read returns one shared function per method that calls invoke on its `this` and raises Node's ERR_INVALID_THIS TypeError (or a rejected promise for the async operations) on another receiver. The in operator sees the methods and the modeled properties. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_dyn.h | 23 +++++ packages/runtime/src/scr_fetch.c | 64 +++++++++++-- packages/runtime/src/scr_json.c | 87 ++++++++++++++++- tests/corpus/7740-handle-method-values.js | 110 ++++++++++++++++++++++ 4 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 tests/corpus/7740-handle-method-values.js diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..5251c6f6c 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1472,6 +1472,19 @@ ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule); * (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls * them exactly when the unit is linked, the scr_net_install story), so * this always-linked core never references gated units. */ +/* A method a handle class answers through invoke, read as a value + * (`typeof req.json`, `raw[key]()`): the read answers one function per + * (tag, method) that calls invoke on its `this`. length is the method's + * Function.prototype.length; rejects marks a method whose call on another + * receiver answers a rejected promise instead of throwing (the async + * WebIDL operations: Body's json(), ReadableStream's cancel()). */ +typedef struct ScrDynHandleMethod { + const char *name; + uint32_t length; + bool rejects; + const char *invalid_this; /* NULL = the class's (ScrDynHandleOps) */ +} ScrDynHandleMethod; + typedef struct ScrDynHandleOps { const char *cls; /* Node's constructor name — error texts and dynCheck's "got ..." */ void *(*retain)(void *h); @@ -1537,6 +1550,16 @@ typedef struct ScrDynHandleOps { * stream unit builds the handle's Transform from these operations on its * first stream use. NULL = the class is not a stream. */ const struct ScrDynHandleTransform *transform; + /* The class's methods as values, terminated by a NULL name; NULL = a + * property read answers no methods (member calls still reach invoke). + * See ScrDynHandleMethod. */ + const ScrDynHandleMethod *methods; + /* The TypeError (code ERR_INVALID_THIS) a method value called on another + * receiver raises; NULL = WebIDL's "Illegal invocation". */ + const char *invalid_this; + /* A promise rejected with the pending exception (+1), for the rejects + * methods; the owning unit supplies it, since promises are a gated unit. */ + ScrDyn *(*rejected)(void); } ScrDynHandleOps; /* The operations a Transform handle gives the stream unit (scr_stream.c): diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 7cc2beef4..916fc6bf9 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4990,7 +4990,7 @@ static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) { if (default_value) { ScrStr *s = scr_str_new(default_value, strlen(default_value)); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; } if (sf_name(key, len, "keepalive") || sf_name(key, len, "isReloadNavigation") || sf_name(key, len, "isHistoryNavigation")) return scr_dyn_new_bool(false); if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len); - return scr_dyn_undefined(); + return NULL; } static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfRequest *r = ptr; @@ -5495,33 +5495,73 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, return false; } +/* The members invoke answers, as values (ScrDynHandleMethod), with Node's + * lengths and the brand check each one runs on another receiver. */ +static const ScrDynHandleMethod sf_signal_methods[] = { + {"throwIfAborted", 0, false, + "Value of \"this\" must be of type AbortSignal"}, + {"addEventListener", 2, false}, + {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, + {NULL, 0, false}}; +static const ScrDynHandleMethod sf_stream_methods[] = { + {"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_reader_methods[] = { + {"read", 0, true}, {"releaseLock", 0, false}, {"cancel", 0, true}, + {NULL, 0, false}}; +static const ScrDynHandleMethod sf_body_methods[] = { + {"json", 0, true}, {"text", 0, true}, {"bytes", 0, true}, + {"arrayBuffer", 0, true}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_headers_methods[] = { + {"append", 2, false}, {"delete", 1, false}, {"entries", 0, false}, + {"forEach", 1, false}, {"get", 1, false}, {"getSetCookie", 0, false}, + {"has", 1, false}, {"keys", 0, false}, {"set", 2, false}, + {"values", 0, false}, {NULL, 0, false}}; + +static ScrDyn *sf_rejected(void) { + ScrPromise *p = scr_promise_settled_ref(NULL, &scr_dyn_retain_v, + &scr_dyn_release_v, NULL); + ScrDyn *out = scr_dyn_new_promise(p); + scr_promise_release(p); + return out; +} + static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, - &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL}; + &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, + .methods = sf_signal_methods, + .invalid_this = "Value of \"this\" must be of type EventTarget"}; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_stream_ops = { "ReadableStream", &sf_stream_retain_v, &sf_stream_release_v, - &sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL}; + &sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL, + .methods = sf_stream_methods, + .invalid_this = "Value of \"this\" must be of type ReadableStream", + .rejected = &sf_rejected}; static const ScrDynHandleOps sf_reader_ops = { "ReadableStreamDefaultReader", &sf_reader_handle_retain_v, &sf_reader_handle_release_v, &sf_reader_invoke, &sf_reader_get, - &sf_no_set, NULL}; + &sf_no_set, NULL, .methods = sf_reader_methods, + .invalid_this = + "Value of \"this\" must be of type ReadableStreamDefaultReader", + .rejected = &sf_rejected}; static const ScrDynHandleOps sf_controller_ops = { "ReadableStreamDefaultController", &sf_stream_retain_v, &sf_stream_release_v, &sf_controller_invoke, &sf_controller_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_request_ops = { "Request", &sf_request_retain_v, &sf_request_release_v, - &sf_request_invoke, &sf_request_get, &sf_no_set, NULL}; + &sf_request_invoke, &sf_request_get, &sf_no_set, NULL, + .methods = sf_body_methods, .rejected = &sf_rejected}; static const ScrDynHandleOps sf_response_ops = { "Response", &sf_response_retain_v, &sf_response_release_v, - &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; + &sf_response_invoke, &sf_response_get, &sf_no_set, NULL, + .methods = sf_body_methods, .rejected = &sf_rejected}; static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, - &sf_headers_step}; + &sf_headers_step, .methods = sf_headers_methods}; static void sf_teardown(void) { while (sf_live) { @@ -5572,9 +5612,17 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *valu (void)ptr; (void)key; (void)len; (void)value; return false; } +static const ScrDynHandleMethod sf_signal_methods[] = { + {"throwIfAborted", 0, false, + "Value of \"this\" must be of type AbortSignal"}, + {"addEventListener", 2, false}, + {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, + {NULL, 0, false}}; static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, - &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL }; + &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, + .methods = sf_signal_methods, + .invalid_this = "Value of \"this\" must be of type EventTarget" }; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL }; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..5fab5d26e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4975,19 +4975,103 @@ ScrDyn *scr_dyn_promise_key_get(const ScrDyn *d, const ScrStr *k) { return method ? method : scr_dyn_retain(scr_dyn_undefined()); } +/* ── handle methods as values (ScrDynHandleMethod) ────────────────── + * One function per (tag, method), as Node's prototype methods are shared + * by every instance. The function calls invoke on its `this` when that is + * a handle of its tag, and otherwise raises the class's ERR_INVALID_THIS + * TypeError (or answers a promise rejected with it), as the WebIDL brand + * checks do. closure->fn holds the method entry, caps[0] the tag. */ +static SCR_TL ScrDyn **scr_dynh_method_values[SCR_DYNH_CAPACITY]; +static SCR_TL bool scr_dynh_method_values_registered; + +static void scr_dynh_method_values_cleanup(void) { + for (size_t tag = 0; tag < SCR_DYNH_CAPACITY; tag++) { + ScrDyn **values = scr_dynh_method_values[tag]; + if (!values) continue; + scr_dynh_method_values[tag] = NULL; + const ScrDynHandleMethod *methods = scr_dynh_ops[tag]->methods; + for (size_t i = 0; methods[i].name; i++) + if (values[i]) scr_dyn_release(values[i]); + free(values); + } +} + +static ScrDyn *scr_dynh_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + const ScrDynHandleMethod *method = closure->fn; + ScrDynHandleTag tag = (ScrDynHandleTag)scr_box_get_f64(closure->caps[0]); + const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag); + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result; + if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == tag) { + result = ops->invoke(self->v.handle.ptr, self, method->name, args, argc, method->name); + } else { + const char *message = method->invalid_this ? method->invalid_this + : ops->invalid_this ? ops->invalid_this + : "Illegal invocation"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), "ERR_INVALID_THIS"); + result = method->rejects && ops->rejected ? ops->rejected() : NULL; + } + scr_dyn_release(self); + return result; +} + +/* The method `key` of the handle's class as a function value (+1), or NULL + * when the class has no such method. */ +static ScrDyn *scr_dynh_method_value(ScrDynHandleTag tag, const ScrDynHandleOps *ops, const char *key, size_t len) { + const ScrDynHandleMethod *methods = ops->methods; + if (!methods || !ops->invoke) return NULL; + for (size_t i = 0; methods[i].name; i++) { + if (strlen(methods[i].name) != len || memcmp(methods[i].name, key, len) != 0) continue; + if (!scr_dynh_method_values[tag]) { + size_t count = i + 1; + while (methods[count].name) count++; + scr_dynh_method_values[tag] = calloc(count, sizeof(ScrDyn *)); + if (!scr_dynh_method_values[tag]) scr_trap("scriptc: out of memory\n"); + if (!scr_dynh_method_values_registered) { + scr_dynh_method_values_registered = true; + scr_atexit(scr_dynh_method_values_cleanup); + } + } + ScrDyn **slot = &scr_dynh_method_values[tag][i]; + if (!*slot) { + ScrClosure *closure = scr_closure_new((void *)&methods[i], 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], (double)tag); + *slot = scr_dyn_new_func(closure, scr_dynh_method_call, methods[i].length, "", methods[i].name); + } + return scr_dyn_retain(*slot); + } + return NULL; +} + ScrDyn *scr_dyn_handle_key_get(const ScrDyn *d, const ScrStr *k) { - const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag); + ScrDynHandleTag tag = d->v.handle.tag; + const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag); ScrDyn *r = ops->get(d->v.handle.ptr, k->data, k->len); if (scr_exc_pending()) { scr_dyn_release(r); return NULL; } + if (!r) r = scr_dynh_method_value(tag, ops, k->data, k->len); /* Unmodeled names answer undefined — the checked-dynamic tree's own-property stance * (real-but-unmodeled members throw "not supported yet" inside ops->get instead; * SEMANTICS.md documents the remainder). */ return r ? r : scr_dyn_retain(scr_dyn_undefined()); } +/* `key in handle`: the class's methods, and the properties its get models. */ +static bool scr_dyn_handle_in(const ScrDyn *d, const ScrStr *k) { + const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag); + const ScrDynHandleMethod *methods = ops->invoke ? ops->methods : NULL; + for (size_t i = 0; methods && methods[i].name; i++) + if (strlen(methods[i].name) == k->len && memcmp(methods[i].name, k->data, k->len) == 0) return true; + if (!methods) return false; + ScrDyn *value = ops->get(d->v.handle.ptr, k->data, k->len); + if (!value) return scr_dyn_object_proto_name(k); + scr_dyn_release(value); + return true; +} + /* ── the ambient receiver (scr_runtime.h's design note) ─────────────── * A strictly nested push/pop stack: firing sites bind the owner around * each listener call, dyn OBJ method dispatch binds the object, and the @@ -9196,6 +9280,7 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { } if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key); if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key); + if (v->kind == SCR_DYN_HANDLE) return scr_dyn_handle_in(v, key); if (scr_dyn_has_key(v, key)) return true; if (scr_exc_pending()) return false; if (v->kind == SCR_DYN_OBJ) return scr_dyn_builtin_inherited(v, key); diff --git a/tests/corpus/7740-handle-method-values.js b/tests/corpus/7740-handle-method-values.js new file mode 100644 index 000000000..3694c204f --- /dev/null +++ b/tests/corpus/7740-handle-method-values.js @@ -0,0 +1,110 @@ +// Methods of native Web objects read as values in untyped code: hono's +// HonoRequest calls `raw[key]()` with a computed body-method name, and code +// probes `typeof x.json` or `"get" in headers`. Each method is one shared +// function that runs on its `this`; another receiver fails Node's brand check. +const report = (label, e) => console.log(label, e.name, e.message, e.code); +// An untyped holder, as values reach a JavaScript library. +const untyped = (value) => { + const holder = JSON.parse("{}"); + holder.value = value; + return holder.value; +}; + +async function bodies() { + const holder = JSON.parse("{}"); + holder.req = new Request("http://example.test/books", { method: "POST", body: '{"title":"Dune"}' }); + const cache = {}; + const cachedBody = (key) => { + const raw = holder.req; + if (cache[key]) return cache[key]; + return (cache[key] = raw[key]()); + }; + console.log(await cachedBody("json")); + for (const key of ["text", "arrayBuffer", "bytes"]) { + const res = new Response("abc"); + const value = await res[key](); + console.log(key, typeof value, value instanceof ArrayBuffer, value.length ?? value.byteLength); + } +} + +function shape() { + const req = JSON.parse("{}"); + req.value = new Request("http://example.test/"); + const r = req.value; + console.log(typeof r.json, typeof r.text, typeof r.blobby, typeof r.url); + console.log("json" in r, "url" in r, "toString" in r, "missing" in r); + const other = JSON.parse("{}"); + other.res = new Response("x"); + console.log(r.json === r.json, r.json === other.res.json, r.json === r.text); + console.log(r.json.name, r.json.length, r.arrayBuffer.name); + const box = { h: untyped(new Headers({ a: "1" })) }; + console.log(box.h.append.length, box.h.forEach.length, box.h.get.name, "has" in box.h, "nope" in box.h); + const name = "get"; + console.log(box.h[name]("a"), box.h["has"]("b")); + box.h["set"]("b", "2"); + const entries = []; + box.h["forEach"]((v, k) => entries.push(k + "=" + v)); + console.log(entries.join(",")); +} + +async function receivers() { + const box = { + h: untyped(new Headers({ a: "1" })), + req: untyped(new Request("http://example.test/", { method: "POST", body: "x" })), + }; + const get = box.h.get; + console.log(get.call(box.h, "a"), get.bind(box.h)("a"), get.apply(box.h, ["a"])); + try { + get("a"); + } catch (e) { + report("unbound get", e); + } + try { + get.call(box.req, "a"); + } catch (e) { + report("foreign get", e); + } + const text = box.req.text; + const pending = text(); + console.log("text returns", pending instanceof Promise); + await pending.then( + () => console.log("unexpected"), + (e) => report("unbound text", e), + ); + await text.call(box.h).catch((e) => report("foreign text", e)); + console.log(await text.call(box.req)); + const signal = { s: untyped(new AbortController().signal) }; + const throwIfAborted = signal.s.throwIfAborted; + try { + throwIfAborted(); + } catch (e) { + report("unbound throwIfAborted", e); + } + try { + signal.s.addEventListener.call(box.h, "abort", () => {}); + } catch (e) { + report("foreign addEventListener", e); + } +} + +async function streams() { + const box = { body: untyped(new Response("stream").body) }; + const key = "getReader"; + const reader = box.body[key](); + const read = reader.read; + const first = await read.call(reader); + console.log(first.done, new TextDecoder().decode(first.value)); + await read().catch((e) => report("unbound read", e)); + try { + box.body.getReader.call(reader); + } catch (e) { + report("foreign getReader", e); + } +} + +(async () => { + await bodies(); + shape(); + await receivers(); + await streams(); +})(); From 8d6bb60847dc0a25dd462fb34a6ea8897b79750e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:20:31 -0500 Subject: [PATCH 150/169] Test caught values against the class a class binding holds A class binding may hold a subclass of the class it was declared with. When the declared class has subclasses, a caught payload of that class is now tested against the class the binding holds, as for other operands, so a base-class instance no longer passes a test against a subclass. --- .../lowering/expressions/relational.ts | 42 +++++++++++++++++-- .../7734-caught-instanceof-class-binding.js | 20 +++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 9f06cc377..f4b9a59c8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -401,18 +401,52 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // folds statically exactly like the named-target folds below. const rhsClassval = storedClassValueType(lowerer, expr.right); // A catch binding against a class held by a binding (`var E = class - // extends Error {}`): the binding holds exactly that class, so the test - // is the class's caught test, after the right operand's read. + // extends Error {}`). The binding holds the declared class or one of its + // subclasses. Without subclasses the test is the class's caught test, + // after the right operand's read; otherwise a payload of the declared + // class is tested against the class the binding holds. const caught = lowerer.caughtLocalOf(expr.left); const caughtTarget = rhsClassval?.kind === "classval" && caught ? lowerer.classes.get(rhsClassval.className) : undefined; if (caught && caughtTarget && !caughtTarget.localClass && !caughtTarget.def.runtime) { + const classValue = lowerer.lowerExpr(expr.right); + if (caughtTarget.subclasses.length === 0) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: classValue, loc }], + result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + type: BOOL, + loc, + }; + } + if (classValue.type.kind !== "classval") + lowerer.badType(expr.right, lowerer.typeOf(expr.right)); + const held = lowerer.declareHiddenLocal("%instanceofClass", classValue.type); + const instance: IrType = { kind: "object", className: caughtTarget.def.name }; return { kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.right), loc }], - result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + stmts: [{ kind: "varDecl", localId: held.id, init: classValue, loc }], + result: { + kind: "logical", + op: "&&", + left: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + right: { + kind: "instanceOfValue", + value: { + kind: "caughtNarrow", + value: varRef(caught.id, CAUGHT, loc), + type: instance, + loc, + }, + classValue: varRef(held.id, classValue.type, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, type: BOOL, loc, }; diff --git a/tests/corpus/7734-caught-instanceof-class-binding.js b/tests/corpus/7734-caught-instanceof-class-binding.js index 55ee792ac..4332daab0 100644 --- a/tests/corpus/7734-caught-instanceof-class-binding.js +++ b/tests/corpus/7734-caught-instanceof-class-binding.js @@ -20,3 +20,23 @@ console.log(pick("/a"), pick("/b/*")); try { throw new Error("x"); } catch (e) { console.log(e instanceof UnsupportedPathError, e instanceof Error); } const u = new UnsupportedPathError("m"); console.log(u instanceof UnsupportedPathError, u.message, u.name, u instanceof Error); + +// A binding that holds a subclass of the class it was declared with: the +// test reads the class the binding holds at run time. +class BaseError extends Error {} +class DerivedError extends BaseError {} +class LeafError extends DerivedError {} +var Current = BaseError; +function caughtIs(value) { + try { + throw value; + } catch (e) { + return e instanceof Current; + } +} +const samples = () => [new BaseError("b"), new DerivedError("d"), new LeafError("l"), new Error("e"), "text"]; +console.log("base", samples().map(caughtIs).join(",")); +Current = DerivedError; +console.log("derived", samples().map(caughtIs).join(",")); +Current = LeafError; +console.log("leaf", samples().map(caughtIs).join(",")); From 3cbd1740a83de5d2b32a21863c5575853c142738 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:28:46 -0700 Subject: [PATCH 151/169] Call methods of bag-tracked class instances without building their views Untyped code calling a method on an instance of a compiled JavaScript class whose own properties live in a property bag materialized the instance's whole checked view on every call (copying the bag, boxing each field and attaching the prototype) only to read one member. Hono's Context declares its methods as arrow-function fields, so every c.header() or c.json() from middleware rebuilt an 18-property view. The materializer of such a class gives the view exactly the bag's string-keyed properties, the declared fields among them with their native slot values, and the class's prototype. The compiler now emits those parts as ScrDynTypedBagClassOps (bag, prototype, declared field names, field getter) and binds them to the class's capsules, and scr_dyn_invoke resolves a member that is an ordinary data property, own or inherited, from them, calling it with the capsule as this. Accessors, read-only fields, capsule prototype links and everything else still build the view. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/expr-context.ts | 3 + .../src/backend/llvm/expr-stream-bridges.ts | 182 ++++++++++++++---- .../7621-js-class-field-methods.js.json | 6 + packages/runtime/src/scr_dyn.h | 41 +++- packages/runtime/src/scr_dyn_invoke.c | 14 ++ packages/runtime/src/scr_json.c | 73 ++++++- tests/corpus/7621-js-class-field-methods.js | 96 +++++++++ .../7621-js-class-field-methods.js.json | 1 + 8 files changed, 379 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json create mode 100644 tests/corpus/7621-js-class-field-methods.js create mode 100644 tests/harness/corpus-costs/7621-js-class-field-methods.js.json diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index df6dfc608..c6dc506f4 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -46,4 +46,7 @@ export interface LlStreamTypedRefAdapter { recordOps?: string; /** `@sym` of the ScrDynTypedClassOps table for class instance capsules. */ classOps?: string; + /** `@sym` of the ScrDynTypedBagClassOps table for capsules of bag-tracked + * class instances whose views inherit from prototype data. */ + bagClassOps?: string; } diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index ceaca3778..aee7be180 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -767,6 +767,7 @@ export function streamTypedRefMaterializeAdapter( // class view ops are published first. const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined; if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`; + else if (opsMeta && liveBagClassOpsEligible(opsMeta)) adapter.bagClassOps = `@${snapshot}_bops`; adapter.commit = `@${snapshot}_commit`; adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot); const B = new BlockBuilder(); @@ -779,6 +780,7 @@ export function streamTypedRefMaterializeAdapter( ); } if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); + if (adapter.bagClassOps !== undefined) liveBagClassOps(host, snapshot, meta, t); if (meta.hierarchy && meta.children.length) { const pre = B.tmp(); for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType)) @@ -850,11 +852,7 @@ export function streamTypedRefMaterializeAdapter( B.line(`call void @scr_dyn_release_v(ptr ${prototype})`); B.line(`call void @scr_dyn_release_v(ptr ${attached})`); } - for (const field of meta.def.fields.filter( - (f) => - isClassOwnEnumerableFieldName(f.name) && - !(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")), - )) { + for (const field of classViewFields(meta)) { const afterField = meta.def.tracksOwnFields ? B.newLabel("field.after") : null; if (afterField) { host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`); @@ -868,32 +866,7 @@ export function streamTypedRefMaterializeAdapter( B.condBr(present, yes, afterField); B.startBlock(yes); } - const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); - const fieldPtr = B.tmp(); - let fieldValue = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, - ); - if (host.int32Slots.isField(t.className, field.name)) { - const raw = B.tmp(); - B.line(`${raw} = load i32, ptr ${fieldPtr}`); - B.line(`${fieldValue} = sitofp i32 ${raw} to double`); - } else if (host.inlineUnions.classField(t.className, field.name)) - fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); - else if (compressed) { - const idx = B.tmp(); - B.line(`${idx} = load i32, ptr ${fieldPtr}`); - for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) - B.line(line.trimStart()); - } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); - if (llFieldType(field.type, false) === "i8") { - const boolValue = B.tmp(); - B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); - fieldValue = boolValue; - } - const nullable = host.nullableFields.get(t.className, field.name); - if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + const { boxed, nullable, fieldValue } = classFieldDynValue(host, B, meta, t, field); // Attributes copied from the bag do not stop the view taking the value. const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; if (withAttributes) @@ -1491,14 +1464,157 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM ); } +/** The checked-dynamic value a class instance's view holds for `field`, read + * from its native slot of `%p`: `boxed` (+1), and the nullable union + * `fieldValue` the caller releases when `nullable` is set. */ +function classFieldDynValue( + host: LlvmEmitterContext, + B: BlockBuilder, + meta: LlClassMeta, + t: Extract, + field: LlClassMeta["def"]["fields"][number], +): { boxed: string; nullable: NullableRefField | null; fieldValue: string } { + const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); + const fieldPtr = B.tmp(); + let fieldValue = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, + ); + if (host.int32Slots.isField(t.className, field.name)) { + const raw = B.tmp(); + B.line(`${raw} = load i32, ptr ${fieldPtr}`); + B.line(`${fieldValue} = sitofp i32 ${raw} to double`); + } else if (host.inlineUnions.classField(t.className, field.name)) + fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); + else if (compressed) { + const idx = B.tmp(); + B.line(`${idx} = load i32, ptr ${fieldPtr}`); + for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) + B.line(line.trimStart()); + } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); + if (llFieldType(field.type, false) === "i8") { + const boolValue = B.tmp(); + B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); + fieldValue = boolValue; + } + const nullable = host.nullableFields.get(t.className, field.name); + if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); + return { boxed: host.streamTypedRefBoxValue(B, field.type, fieldValue), nullable, fieldValue }; +} + +/** The fields an instance's view takes from their native slots, in order: + * its own enumerable fields, except an Error's message and name. */ +function classViewFields(meta: LlClassMeta): LlClassMeta["def"]["fields"] { + return meta.def.fields.filter( + (f) => + isClassOwnEnumerableFieldName(f.name) && + !(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")), + ); +} + +/** Classes whose instance views the materializer builds from the property + * bag and the prototype data alone (see liveBagClassOps): JavaScript classes + * tracking their own fields, outside the Error and runtime families, with no + * subclass whose view another materializer builds. */ +function liveBagClassOpsEligible(meta: LlClassMeta): boolean { + return ( + meta.def.tracksOwnFields === true && + (meta.def.prototypeDataHelper !== undefined || + meta.def.instancePrototypeHelper !== undefined) && + !meta.def.runtime && + !meta.root.def.runtime && + meta.root.def.name !== "%Error" && + !(meta.hierarchy && meta.children.length > 0) && + meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + !meta.def.fields.some( + (field) => + field.name === CLASS_ATTRIBUTES || + meta.def.fields.some((f) => f.name === classFieldAttributesName(field.name)), + ) + ); +} + +/** The hooks of a bag-tracked class instance capsule + * (ScrDynTypedBagClassOps). The materializer gives the instance's view + * exactly the bag's string-keyed properties, the values of the declared + * fields among them from their native slots, and the prototype these hooks + * return, so a method call resolves without building the view. */ +function liveBagClassOps( + host: LlvmEmitterContext, + snapshot: string, + meta: LlClassMeta, + t: Extract, +): void { + const struct = classStructSym(t.className); + const bag = new BlockBuilder(); + const slot = bag.tmp(), + value = bag.tmp(); + bag.line( + `${slot} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${classFieldIndex(meta, DYN_CLASS_PROPERTIES).index}`, + ); + bag.line(`${value} = load ptr, ptr ${slot}`); + bag.terminate(`ret ptr ${value}`); + const proto = new BlockBuilder(); + const prototype = proto.tmp(); + if (meta.def.instancePrototypeHelper) { + const receiver = proto.tmp(); + proto.line(`${receiver} = call ptr ${vAdapters(host.shapeHost, t).retain}(ptr %p)`); + proto.line( + `${prototype} = call ptr @${mangleFunction(meta.def.instancePrototypeHelper)}(ptr ${receiver})`, + ); + } else proto.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper!)}()`); + proto.terminate(`ret ptr ${prototype}`); + // Field `%i` of the view's field list, as the materializer boxes it. + const fields = classViewFields(meta); + const get = new BlockBuilder(); + const cases = fields.map((_, i) => get.newLabel(`field.${i}`)); + const none = get.newLabel("field.none"); + get.terminate( + `switch ${host.sizeType} %i, label %${none} [ ${cases.map((label, i) => `${host.sizeType} ${i}, label %${label}`).join(" ")} ]`, + ); + fields.forEach((field, i) => { + get.startBlock(cases[i]!); + const { boxed, nullable, fieldValue } = classFieldDynValue(host, get, meta, t, field); + if (nullable) { + host.declare(`declare void @scr_union_release(ptr)`); + get.line(`call void @scr_union_release(ptr ${fieldValue})`); + } + get.terminate(`ret ptr ${boxed}`); + }); + get.startBlock(none); + get.terminate(`ret ptr null`); + const names = fields.map((field) => `ptr ${host.internLiteral(field.name)}`); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_field(ptr %p, ${host.sizeType} %i) ${FN_ATTRS} { ; view field of ${meta.def.name}`, + get.render(), + `}`, + ``, + `@${snapshot}_fnames = internal constant [${names.length} x ptr] ${names.length ? `[${names.join(", ")}]` : "zeroinitializer"}`, + ); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_bag(ptr %p) ${FN_ATTRS} { ; property bag of ${meta.def.name}`, + bag.render(), + `}`, + ``, + `define internal ptr @${snapshot}_proto(ptr %p) ${FN_ATTRS} { ; view prototype of ${meta.def.name}`, + proto.render(), + `}`, + ``, + `@${snapshot}_bops = internal constant { ptr, ptr, ${host.sizeType}, ptr, ptr } { ptr @${snapshot}_bag, ptr @${snapshot}_proto, ${host.sizeType} ${fields.length}, ptr @${snapshot}_fnames, ptr @${snapshot}_field }`, + ``, + ); +} + /** The live view a capsule of this adapter binds: the runtime bind call * and its ops constant, with the view code a deferred receiver recipe - * records (1 array, 2 record, 3 class). */ + * records (1 array, 2 record, 3 class, 4 bag-tracked class). */ export function liveRefView( adapter: LlStreamTypedRefAdapter, -): { bind: string; ops: string; code: 1 | 2 | 3 } | null { +): { bind: string; ops: string; code: 1 | 2 | 3 | 4 } | null { if (adapter.classOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_class", ops: adapter.classOps, code: 3 }; + if (adapter.bagClassOps !== undefined) + return { bind: "scr_dyn_typed_ref_bind_bag_class", ops: adapter.bagClassOps, code: 4 }; if (adapter.recordOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_record", ops: adapter.recordOps, code: 2 }; if (adapter.arrayOps !== undefined) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json new file mode 100644 index 000000000..a7e5fdd14 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7621-js-class-field-methods.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index fdffd73cb..b31457829 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -395,6 +395,25 @@ typedef struct ScrDynTypedClassOps { ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ } ScrDynTypedClassOps; +/* A compiled JavaScript class whose instances record their own properties + * in a property bag and whose views inherit from the class's prototype + * data, emitted per class by the compiler. The view of such an instance is + * a fresh object holding exactly the bag's string-keyed properties, where + * each declared field among them takes its value from the native slot, and + * whose prototype is the class's. A method call can therefore resolve + * without building the view (scr_dyn_typed_ref_bag_method). + * bag: the instance's bag, borrowed; NULL or undefined when it has none; + * prototype: +1 prototype a fresh view of the instance takes; + * names: the declared fields whose values come from native slots; + * field: +1 view value of declared field `index`. */ +typedef struct ScrDynTypedBagClassOps { + ScrDyn *(*bag)(void *instance); + ScrDyn *(*prototype)(void *instance); + size_t count; + ScrStr *const *names; /* immortal field-name strings */ + ScrDyn *(*field)(void *instance, size_t index); +} ScrDynTypedBagClassOps; + struct ScrDyn { size_t rc; /* SIZE_MAX = immortal (unused for dyn; kept per convention) */ ScrDynKind kind; @@ -479,7 +498,9 @@ struct ScrDyn { * keep their ScrDynTypedRecordOps here instead, flagged by `record` * (scr_dyn_typed_ref_bind_record), and class instances their * ScrDynTypedClassOps, flagged by `klass` - * (scr_dyn_typed_ref_bind_class). NULL for other referents. */ + * (scr_dyn_typed_ref_bind_class), or their ScrDynTypedBagClassOps, + * flagged by `bag_class` (scr_dyn_typed_ref_bind_bag_class). NULL for + * other referents. */ const ScrDynTypedArrayOps *array; /* Both caches belong to this capsule. ReadableStream canonicalizes * capsules for repeated source references, so the refreshed dyn view @@ -488,11 +509,12 @@ struct ScrDyn { ScrDyn *materialized; ScrDynTypedCast *casts; /* One word keeps this, the largest payload, at its previous size. */ - uint32_t type_key_len : 28; + uint32_t type_key_len : 27; uint32_t traced : 1; /* native referent has a cycle header */ uint32_t observed : 1; /* native release reports this identity's disposal */ uint32_t record : 1; /* `array` holds ScrDynTypedRecordOps */ uint32_t klass : 1; /* `array` holds ScrDynTypedClassOps */ + uint32_t bag_class : 1; /* `array` holds ScrDynTypedBagClassOps */ } typed_ref; /* SCR_DYN_PROMISE: the retained promise. The boundary contract: it * settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a @@ -629,7 +651,9 @@ static inline const char *scr_dyn_class_name(const ScrDyn *d) { /* The element hooks of a live array capsule, or NULL (including for record * and class capsules, which keep their hooks in the same slot). */ static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDyn *d) { - return d->v.typed_ref.record || d->v.typed_ref.klass ? NULL : d->v.typed_ref.array; + return d->v.typed_ref.record || d->v.typed_ref.klass || d->v.typed_ref.bag_class + ? NULL + : d->v.typed_ref.array; } /* BORROWED member lookup on a SCR_DYN_OBJ; NULL when the key is absent. */ @@ -1029,6 +1053,17 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec); /* Attach the prototype member names of a class instance capsule * (idempotent), so `in` sees its methods and accessors. */ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); +/* Attach the bag and prototype hooks of a capsule of a bag-tracked class + * instance (idempotent). */ +void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops); +/* The method `key` that a call on a bag-tracked class instance capsule + * reads from its view, +1, when an ordinary data property answers it: an + * own one (a declared field's value from its native slot, or another bag + * property's value), or else one on the view's prototype chain. NULL when + * only the view can answer (the caller builds it), and also NULL, with the + * exception pending, when the prototype hook threw. Never runs user + * code. */ +ScrDyn *scr_dyn_typed_ref_bag_method(const ScrDyn *d, const char *key, size_t key_len); /* The constructor name of a class instance capsule's own class, for * util.inspect; NULL when the capsule is not a bound class instance, its * class is anonymous, or its prototype was replaced. */ diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 4039fda94..364017e05 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -879,6 +879,20 @@ static ScrDyn *scr_dyn_invoke_impl( scr_dyn_release(date_method); return result; } + /* A method of a bag-tracked class instance that its view holds as an + * ordinary data property, own or inherited: the call the view's + * dispatch below makes, without building the view. */ + size_t method_len = strlen(method); + ScrDyn *inherited = scr_dyn_typed_ref_bag_method(recv, method, method_len); + if (scr_exc_pending()) return NULL; + if (inherited && scr_dyn_is_callable(inherited)) { + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(inherited, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(inherited); + return result; + } + scr_dyn_release(inherited); ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); if (scr_exc_pending()) { scr_dyn_release(materialized); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 907486fea..f0bd04d97 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3414,7 +3414,7 @@ static ScrDyn *scr_dyn_typed_ref_new( return scr_dyn_retain(value); } } - if (type_key_len >= (size_t)1 << 28) scr_trap("scriptc: typed reference key too long\n"); + if (type_key_len >= (size_t)1 << 27) scr_trap("scriptc: typed reference key too long\n"); ScrDyn *d = scr_dyn_alloc(SCR_DYN_TYPED_REF); d->v.typed_ref.ptr = retain(ptr); d->v.typed_ref.retain = retain; @@ -3437,6 +3437,7 @@ static ScrDyn *scr_dyn_typed_ref_new( d->v.typed_ref.observed = observed; d->v.typed_ref.record = false; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; scr_typed_ref_slots[slot] = d; /* the empty slot that ended the probe */ scr_typed_ref_count++; return d; @@ -3652,6 +3653,7 @@ void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops) { d->v.typed_ref.array = ops; d->v.typed_ref.record = false; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; } } @@ -3670,6 +3672,7 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = true; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; } } @@ -3678,9 +3681,75 @@ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = false; d->v.typed_ref.klass = true; + d->v.typed_ref.bag_class = false; } } +void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops) { + if (d && d->kind == SCR_DYN_TYPED_REF) { + d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; + d->v.typed_ref.record = false; + d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = true; + } +} + +/* The view's own string keys are the bag's (ScrDynTypedBagClassOps). An + * own data property answers as the view would hold it: a declared field + * from its native slot (the materializer's ordinary write of that value, + * which a read-only or accessor entry would refuse, so those take the + * view), another property with the bag's value. A key the bag lacks is + * looked up on the prototype the view has: the cached view's, which keeps + * prototype changes made through the view, or for a capsule without one, + * the prototype a fresh view takes. The walk is scr_dyn_obj_read_receiver's + * from the view's prototype on, answering only when it meets an ordinary + * data property before anything that needs more (an accessor, a function, + * array or capsule link, Object.prototype). */ +ScrDyn *scr_dyn_typed_ref_bag_method(const ScrDyn *d, const char *key, size_t key_len) { + if (d->kind != SCR_DYN_TYPED_REF || !d->v.typed_ref.bag_class || d->prototype) return NULL; + const ScrDynTypedBagClassOps *ops = (const ScrDynTypedBagClassOps *)(const void *)d->v.typed_ref.array; + void *instance = d->v.typed_ref.ptr; + const ScrDyn *bag = ops->bag(instance); + if (bag && bag->kind != SCR_DYN_UNDEF) { + if (bag->kind != SCR_DYN_OBJ) return NULL; + const ScrDynEntry *own = scr_dyn_find_entry(bag, key, key_len); + if (own) { + for (size_t i = 0; i < ops->count; i++) { + const ScrStr *name = ops->names[i]; + if (name->len != key_len || memcmp(name->data, key, key_len) != 0) continue; + return own->accessor || !own->writable ? NULL : ops->field(instance, i); + } + return own->accessor ? NULL : scr_dyn_retain(own->value); + } + } + ScrDyn *prototype; + const ScrDyn *cached = d->v.typed_ref.materialized; + if (cached) { + if (cached->kind != SCR_DYN_OBJ || !cached->prototype) return NULL; + prototype = scr_dyn_retain(cached->prototype); + } else { + prototype = ops->prototype(instance); + if (scr_exc_pending()) { + scr_dyn_release(prototype); + return NULL; + } + if (!prototype) return NULL; + } + ScrDyn *found = NULL; + for (const ScrDyn *current = prototype; current; current = current->prototype) { + if (current == scr_builtin_object_prototype || current->kind == SCR_DYN_FUNC || + current->kind == SCR_DYN_ARR || current->kind == SCR_DYN_TYPED_REF) + break; + if (current->kind != SCR_DYN_OBJ) continue; + ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len); + if (!entry) continue; + if (!entry->accessor) found = scr_dyn_retain(entry->value); + break; + } + scr_dyn_release(prototype); + return found; +} + static const ScrDynTypedClassOps *scr_typed_ref_class_ops(const ScrDyn *d) { if (!d->v.typed_ref.klass) return NULL; const ScrDynTypedClassOps *ops = (const ScrDynTypedClassOps *)(const void *)d->v.typed_ref.array; @@ -5049,6 +5118,8 @@ ScrDyn *scr_dyn_this_get(void) { scr_dyn_typed_ref_bind_record(e->dv, (const ScrDynTypedRecordOps *)r->ops); else if (r->view == 3) scr_dyn_typed_ref_bind_class(e->dv, (const ScrDynTypedClassOps *)r->ops); + else if (r->view == 4) + scr_dyn_typed_ref_bind_bag_class(e->dv, (const ScrDynTypedBagClassOps *)r->ops); } if (e->dv) return scr_dyn_retain(e->dv); /* An uninstalled tag never binds: a unit that fires without its dyn diff --git a/tests/corpus/7621-js-class-field-methods.js b/tests/corpus/7621-js-class-field-methods.js new file mode 100644 index 000000000..cd0a5be99 --- /dev/null +++ b/tests/corpus/7621-js-class-field-methods.js @@ -0,0 +1,96 @@ +// Untyped code calling methods on an instance of a JS class whose own +// properties live in a property bag: methods declared as arrow-function +// fields (own properties), prototype methods, prototype methods replaced or +// shadowed after construction, accessors on the prototype, and calls of +// members that are not functions. Each call must see what the instance +// holds at that moment and bind `this` to the instance. +'use strict'; + +class Context { + status = 200; + headers = {}; + label = 'ctx'; + header = (name, value) => { + this.headers[name] = value; + return this; + }; + json = (body) => `${this.status} ${JSON.stringify(body)} ${JSON.stringify(this.headers)}`; + constructor(label) { + this.label = label; + } + describe() { + return `${this.label}/${this.status}`; + } + get upper() { + return this.label.toUpperCase(); + } + shout() { + return this.upper + '!'; + } +} + +// Constant method names: the calls a framework's untyped code makes. +const calls = { + describe: (t) => t.describe(), + header: (t, name, value) => t.header(name, value), + json: (t, body) => t.json(body), + shout: (t) => t.shout(), + extra: (t, n) => t.extra(n), + status: (t) => t.status(), + missing: (t) => t.missing(), + upper: (t) => t.upper(), +}; + +function call(target, method, ...args) { + return calls[method](target, ...args); +} + +function put(target, key, value) { + target[key] = value; +} + +const c = new Context('first'); +console.log(call(c, 'describe')); +console.log(call(call(c, 'header', 'x-a', '1'), 'json', { ok: true })); +put(c, 'status', 404); +console.log(call(c, 'json', [1, 2]), call(c, 'describe'), call(c, 'shout')); + +// A field method replaced through untyped code, and an own property that +// shadows a prototype method. +put(c, 'json', (body) => `replaced ${body}`); +put(c, 'describe', function () { + return `own describe of ${this.label}`; +}); +console.log(call(c, 'json', 'x'), call(c, 'describe')); + +// A prototype method replaced after construction reaches every instance +// that does not shadow it. +const d = new Context('second'); +Context.prototype.describe = function () { + return `patched ${this.label}`; +}; +console.log(call(d, 'describe'), call(c, 'describe')); + +// A prototype method added after construction. +Context.prototype.extra = function (n) { + return `${this.label} extra ${n}`; +}; +console.log(call(d, 'extra', 3)); + +// Members that are not functions. +for (const name of ['status', 'missing', 'upper']) { + try { + call(d, name); + } catch (error) { + console.log(error instanceof TypeError, error.message); + } +} + +// Many calls in a row stay consistent with the instance's current state. +let out = ''; +for (let i = 0; i < 4; i++) { + call(d, 'header', `h${i}`, String(i)); + put(d, 'status', 200 + i); + out += call(d, 'json', i) + '\n'; +} +process.stdout.write(out); diff --git a/tests/harness/corpus-costs/7621-js-class-field-methods.js.json b/tests/harness/corpus-costs/7621-js-class-field-methods.js.json new file mode 100644 index 000000000..ef439d096 --- /dev/null +++ b/tests/harness/corpus-costs/7621-js-class-field-methods.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4500 } From 5aef6597ab95b604ce3514c011ee49823aed68b3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:28:47 -0700 Subject: [PATCH 152/169] Test the function tag before comparing a class constructor's key dyn.classIs went through the generic library call: retain the receiver, call scr_dyn_class_is_key, release it (and buffer it as a cycle candidate). A class's static property bag dispatch (%dyn.class.staticProperties) runs one probe per compiled class for every untyped property read, so this cost about 500 instructions per read in a Hono app. The probe now tests the value's function tag inline, as the capsule probes test theirs, so LLVM skips the chain for every other value, and borrows the receiver across a literal key. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/emitter.test.ts | 27 ++++++++++++++ .../compiler/src/backend/llvm/lib-dispatch.ts | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index d1fd62a83..4793663aa 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -459,6 +459,33 @@ test("class brand probes borrow a local only across an inert literal key", () => expect(computed).toContain("call ptr @scr_dyn_retain_v("); }); +test("class constructor probes test the function tag and borrow across a literal key", () => { + const dyn: IrType = { kind: "dyn" }; + const key: IrExpr = { kind: "strLit", value: "classval:Example", type: STRING, loc: receiverLoc }; + const probe: IrExpr = { + kind: "libCall", + fn: "dyn.classIs", + args: [ref(dyn), key], + type: BOOL, + loc: receiverLoc, + }; + const borrowed = work(probe, dyn); + expect(borrowed).not.toContain("call ptr @scr_dyn_retain_v("); + expect(borrowed).toContain("icmp ne ptr"); + expect(borrowed).toContain("icmp eq i32 %t"); + expect(borrowed).toMatch(/icmp eq i32 %t\d+, 8\b/); + expect(borrowed).toContain("call zeroext i1 @scr_dyn_class_is_key("); + const computed = work( + { + ...probe, + args: [ref(dyn), { kind: "toString", operand: ref(dyn), type: STRING, loc: receiverLoc }], + }, + dyn, + ); + expect(computed).toContain("call ptr @scr_dyn_retain_v("); + expect(computed).toContain("call zeroext i1 @scr_dyn_class_is_key("); +}); + test("class brand probes compare the program's key constant before the key text", () => { const dyn: IrType = { kind: "dyn" }; const key: IrExpr = { kind: "strLit", value: "object:Example", type: STRING, loc: receiverLoc }; diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..eaf8f59f6 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -297,6 +297,41 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`${result} = load i1, ptr ${slot}`); return { name: result, type: e.type }; } + if (e.fn === "dyn.classIs") { + // Only a class constructor's function value can match. Expose the + // function tag to LLVM so a chain of constructor probes (a static + // property bag's dispatch) costs one test for every other value, and + // borrow the receiver across a literal key as the capsule probes do. + const literal = e.args[1]!.kind === "strLit"; + const value = literal ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); + const key = host.emitExpr(e.args[1]!); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca i1`); + B.line(`store i1 false, ptr ${slot}`); + const present = B.tmp(); + const inspect = B.newLabel("ctor.inspect"); + const compare = B.newLabel("ctor.compare"); + const done = B.newLabel("ctor.done"); + B.line(`${present} = icmp ne ptr ${value.name}, null`); + B.condBr(present, inspect, done); + B.startBlock(inspect); + const kind = host.dynKind(value.name); + const fn = B.tmp(); + B.line(`${fn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`); + B.condBr(fn, compare, done); + B.startBlock(compare); + host.declare("declare zeroext i1 @scr_dyn_class_is_key(ptr, ptr)"); + const matches = B.tmp(); + B.line( + `${matches} = call zeroext i1 @scr_dyn_class_is_key(ptr ${value.name}, ptr ${key.name})`, + ); + B.line(`store i1 ${matches}, ptr ${slot}`); + B.br(done); + B.startBlock(done); + const result = B.tmp(); + B.line(`${result} = load i1, ptr ${slot}`); + return { name: result, type: e.type }; + } if (e.fn === "crypto.randomFillCb" || e.fn === "crypto.randomFillCbDyn") { host.usesTimers = true; const args = e.args.map((arg) => host.emitExpr(arg)); From 9e41d505b63d0b573c3d99b0ac165e64789c33a8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:30:06 -0500 Subject: [PATCH 153/169] Declare the property-bag helpers in the runtime header scr_dyn_bag_create_field and scr_dyn_is_class_capsule are declared by the LLVM backend; their prototypes move from scr_json.c into scr_dyn.h, which llvm-runtime-abi.test.ts checks the backend's declares against. --- packages/runtime/src/scr_dyn.h | 2 ++ packages/runtime/src/scr_json.c | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..f7a71538f 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -935,6 +935,8 @@ ScrDyn *scr_dyn_new_obj_null_proto(void); ScrDyn *scr_dyn_obj_create(ScrDyn *prototype); ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors); ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object); +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); +bool scr_dyn_is_class_capsule(const ScrDyn *d); ScrDyn *scr_dyn_get_prototype(ScrDyn *object); ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object); ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..290ae4982 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16215,7 +16215,6 @@ void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { * does not have it yet. Answers false, changing nothing, for any other bag, * whose creation takes the general defineProperty path. Calls only * exported functions. */ -bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { if (!bag || bag->kind != SCR_DYN_OBJ || bag->non_extensible || bag->v.obj.source_access) return false; @@ -16227,7 +16226,6 @@ bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { /* Whether a value is a capsule of a compiled class instance (its type key * names an object type): the class tables' fast exit for every other value, * including record and array capsules. Calls only libc. */ -bool scr_dyn_is_class_capsule(const ScrDyn *d); bool scr_dyn_is_class_capsule(const ScrDyn *d) { return d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.type_key_len > 7 && memcmp(d->v.typed_ref.type_key, "object:", 7) == 0; From 8bb9ee8c5784f4e0d7ec8c7e07b3887c1c2c90de Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:38:18 -0500 Subject: [PATCH 154/169] Check generateKeyPair arguments and RSA parameters as Node does --- .../limitations/builtin-apis/crypto.mdx | 2 + .../src/frontend/lowering/builtins/crypto.ts | 10 +- .../7445-crypto-key-pair-options.mjs.json | 6 + .../7446-crypto-rsa-key-pair-errors.ts.json | 6 + packages/runtime/src/scr_pki.c | 473 ++++++++++++------ tests/corpus/7445-crypto-key-pair-options.mjs | 105 ++++ .../corpus/7446-crypto-rsa-key-pair-errors.ts | 72 +++ .../7445-crypto-key-pair-options.mjs.json | 1 + .../7446-crypto-rsa-key-pair-errors.ts.json | 1 + 9 files changed, 526 insertions(+), 150 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json create mode 100644 tests/corpus/7445-crypto-key-pair-options.mjs create mode 100644 tests/corpus/7446-crypto-rsa-key-pair-errors.ts create mode 100644 tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json create mode 100644 tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 971065408..5d7ff0e56 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -29,6 +29,8 @@ 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. + `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. 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. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 47e66f9b2..6123ee0a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1455,14 +1455,10 @@ function lowerPkiCall( expr, `${supported} is supported`, ); - // generateKeyPair(type, callback): Node reads a function in the options - // position as the callback. - const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2; + // generateKeyPair(type, callback): the runtime reads a function in the + // options position as the callback, as Node does. const values: IrExpr[] = []; - for (let i = 0; i < max; i++) { - const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i]; - values.push(dynArg(lowerer, node, loc)); - } + for (let i = 0; i < max; i++) values.push(dynArg(lowerer, args[i], loc)); if (fn === "crypto.generateKeyPairCb") return { kind: "libCall", fn, args: values, type: VOID, loc }; const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json new file mode 100644 index 000000000..5d57afbef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7445-crypto-key-pair-options.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json new file mode 100644 index 000000000..f055ddab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7446-crypto-rsa-key-pair-errors.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 54aadc06b..888e0eae9 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -68,14 +68,25 @@ static ScrStr *sp_received(const ScrDyn *got, bool range) { } /* ERR_INVALID_ARG_VALUE for an option ("The property 'options.type' is - * invalid. Received 'spki'"). */ -static void sp_property_invalid(const char *name, const ScrDyn *got) { + * invalid. Received 'spki'"), with another reason than "is invalid". */ +static void sp_property_invalid_because(const char *name, const ScrDyn *got, const char *reason) { ScrStr *received = sp_received(got, false); - scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' is invalid. Received %.*s", name, + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' %s. Received %.*s", name, reason, (int)received->len, received->data); scr_str_release(received); } +static void sp_property_invalid(const char *name, const ScrDyn *got) { + sp_property_invalid_because(name, got, "is invalid"); +} + +/* ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS ("The selected key encoding pkcs1 + * can only be used for RSA keys."). */ +static void sp_incompatible(const char *encoding, const char *reason) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", "The selected key encoding %s %s.", encoding, + reason); +} + static void sp_unsupported_input(void) { sp_throw(SCR_ERR_ERROR, "ERR_OSSL_UNSUPPORTED", "error:1E08010C:DECODER routines::unsupported"); } @@ -989,13 +1000,11 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { return NULL; } if (pkcs1 && !sp_is_rsa(k)) { - sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", - "The selected key encoding pkcs1 can only be used for RSA keys."); + sp_incompatible("pkcs1", "can only be used for RSA keys"); return NULL; } if (sec1 && !sp_is_ec(k)) { - sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", - "The selected key encoding sec1 can only be used for EC keys."); + sp_incompatible("sec1", "can only be used for EC keys"); return NULL; } unsigned char *der = malloc(SP_DER_MAX); @@ -1175,132 +1184,292 @@ static void sp_key_install(void) { /* ── generateKeyPairSync ──────────────────────────────────────────────── */ /* A key in the encoding an options.publicKeyEncoding/privateKeyEncoding - * object asks for, or the KeyObject when it is undefined. Consumes `k`. */ + * object asks for, or the KeyObject when it is undefined or null. + * Consumes `k`. */ static ScrDyn *sp_encoded(SpKey *k, const ScrDyn *encoding) { - if (sp_absent(encoding)) return sp_key_box(k); + if (sp_absent(encoding) || encoding->kind == SCR_DYN_NULL) return sp_key_box(k); ScrDyn *result = sp_export(k, encoding); sp_key_release(k); return result; } -static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key); - -static bool sp_check_encoding(const ScrDyn *options, const char *name, bool private_key) { - ScrDyn *encoding = sp_plain(sp_opt(options, name)); - if (!encoding) return false; - bool ok = sp_check_encoding_plain(encoding, name, private_key); - scr_dyn_release(encoding); - return ok; +/* validateObject: an object other than null or an array (typeof + * "object"); false with ERR_INVALID_ARG_TYPE pending. */ +static bool sp_check_object(const ScrDyn *v, const char *name) { + if (v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_ARR && scr_dyn_is_object(v)) return true; + scr_dyn_arg_type_fail(name, "of type object", v); + return false; } -static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key) { - if (sp_absent(encoding)) return true; - char full[64]; - snprintf(full, sizeof full, "options.%s", name); - if (!sp_is_object(encoding)) { - sp_property_invalid(full, encoding); +/* validateUint32 for an option: a number, then an integer, then the + * range; -0 reads as 0. False with the error pending. */ +static bool sp_check_uint32(const ScrDyn *v, const char *name, double *out) { + if (v->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail(name, "of type number", v); return false; } - const ScrDyn *format = sp_opt(encoding, "format"); - if (!sp_is_str(format, "pem") && !sp_is_str(format, "der") && !sp_is_str(format, "jwk")) { - snprintf(full, sizeof full, "options.%s.format", name); - sp_property_invalid(full, format); - return false; - } - if (sp_is_str(format, "jwk")) return true; - const ScrDyn *type = sp_opt(encoding, "type"); - 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"); - if (!valid) { - snprintf(full, sizeof full, "options.%s.type", name); - sp_property_invalid(full, type); + double x = v->v.num; + bool integer = isfinite(x) && trunc(x) == x; + if (!integer || x < 0 || x > 4294967295.0) { + ScrStr *received = sp_received(v, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"%s\" is out of range. It must be %s. Received %.*s", name, + integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); + scr_str_release(received); return false; } + *out = x + 0.0; return true; } -/* Generates the private key for (type, options), validated in Node's - * order; NULL with the error pending. */ -static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { +static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const ScrDyn *type, bool private_key, + const char **refusal); + +/* 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 + * 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, + const char **refusal) { + ScrDyn *enc = sp_plain(sp_opt(options, key)); + if (!enc) return false; + char name[64]; + snprintf(name, sizeof name, "options.%s", key); + bool ok = sp_check_encoding_plain(enc, name, type, private_key, refusal); + scr_dyn_release(enc); + return ok; +} + +static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const ScrDyn *type, bool private_key, + const char **refusal) { + if (sp_absent(enc) || enc->kind == SCR_DYN_NULL) return true; + if (!scr_dyn_is_object(enc)) { + sp_property_invalid(name, enc); + return false; + } + if (!sp_check_object(enc, "options")) return false; + /* Node's isPublic: true for a public key encoding of a non-empty type, + * false for a private key encoding, and unset otherwise. */ + bool is_public = !private_key && type->v.str->len > 0; + char full[96]; + const ScrDyn *format = sp_opt(enc, "format"); + const ScrDyn *kind = sp_opt(enc, "type"); + bool raw_public = sp_is_str(format, "raw-public"); + bool raw_private = sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed"); + bool jwk = sp_is_str(format, "jwk"); + bool der = sp_is_str(format, "der"); + if ((!raw_public && !raw_private && !jwk && !der && !sp_is_str(format, "pem")) || (raw_public && private_key) || + (raw_private && is_public)) { + snprintf(full, sizeof full, "%s.format", name); + sp_property_invalid(full, format); + return false; + } + bool pkcs1 = false, sec1 = false; + if (raw_public || raw_private) { + if (!sp_absent(kind) && + !(raw_public && (sp_is_str(kind, "uncompressed") || sp_is_str(kind, "compressed")))) { + snprintf(full, sizeof full, "%s.type", name); + sp_property_invalid(full, kind); + return false; + } + } else { + pkcs1 = sp_is_str(kind, "pkcs1"); + sec1 = !is_public && sp_is_str(kind, "sec1"); + bool valid = (jwk && sp_absent(kind)) || pkcs1 || sec1 || (!private_key && sp_is_str(kind, "spki")) || + (!is_public && sp_is_str(kind, "pkcs8")); + if (!valid) { + snprintf(full, sizeof full, "%s.type", name); + sp_property_invalid(full, kind); + return false; + } + if (pkcs1 && !sp_is_str(type, "rsa")) { + sp_incompatible("pkcs1", "can only be used for RSA keys"); + return false; + } + if (sec1 && !sp_is_str(type, "ec")) { + sp_incompatible("sec1", "can only be used for EC keys"); + return false; + } + } + bool encrypted = false; + if (!is_public) { + const ScrDyn *cipher = sp_opt(enc, "cipher"); + const ScrDyn *passphrase = sp_opt(enc, "passphrase"); + encrypted = !sp_absent(cipher) && cipher->kind != SCR_DYN_NULL; + if (raw_private) { + if (encrypted || !sp_absent(passphrase)) { + sp_incompatible("raw format", "does not support encryption"); + return false; + } + } else if (encrypted) { + if (cipher->kind != SCR_DYN_STR) { + snprintf(full, sizeof full, "%s.cipher", name); + sp_property_invalid(full, cipher); + return false; + } + if (der && (pkcs1 || sec1)) { + sp_incompatible(pkcs1 ? "pkcs1" : "sec1", "does not support encryption"); + return false; + } + if (passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(passphrase)) { + snprintf(full, sizeof full, "%s.passphrase", name); + sp_property_invalid(full, passphrase); + return false; + } + } else if (!sp_absent(passphrase)) { + snprintf(full, sizeof full, "%s.cipher", name); + sp_property_invalid_because(full, cipher, "is required when a passphrase is specified"); + 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; +} + +/* The key generateKeyPair makes, as sp_keygen_check reads it. */ +typedef struct { + enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519 } kind; + double bits, exponent; /* RSA */ + 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 + * encodings, then the options of the type. False with the error pending. + * The forms this runtime does not generate throw a coded SC2020 error + * after all of these checks. */ +static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen *gen) { if (type->kind != SCR_DYN_STR) { scr_dyn_arg_type_fail("type", "of type string", type); - return NULL; + return false; } - bool rsa = sp_is_str(type, "rsa"); - bool ec = sp_is_str(type, "ec"); - bool ed = sp_is_str(type, "ed25519"); - static const char *const known[] = {"rsa-pss", "dsa", "ed448", "x25519", "x448", "dh"}; - bool other = false; - for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) other = other || sp_is_str(type, known[i]); - if (!rsa && !ec && !ed && !other) { - ScrStr *received = sp_received(type, false); - scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", - "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, - received->data); - scr_str_release(received); - return NULL; + if (!sp_absent(options) && !sp_check_object(options, "options")) return false; + const char *refusal = NULL; + if (!sp_check_encoding(options, "publicKeyEncoding", type, false, &refusal) || + !sp_check_encoding(options, "privateKeyEncoding", type, true, &refusal)) + return false; + bool pss = sp_is_str(type, "rsa-pss"); + if (sp_is_str(type, "rsa") || pss) { + if (!sp_check_object(options, "options")) return false; + gen->kind = SP_GEN_RSA; + if (!sp_check_uint32(sp_opt(options, "modulusLength"), "options.modulusLength", &gen->bits)) return false; + const ScrDyn *exponent = sp_opt(options, "publicExponent"); + gen->exponent = 65537; + if (!sp_absent(exponent) && exponent->kind != SCR_DYN_NULL && + !sp_check_uint32(exponent, "options.publicExponent", &gen->exponent)) + return false; + if (pss && !refusal) refusal = "generateKeyPairSync for this key type"; + } else if (sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { + if (!sp_check_object(options, "options")) return false; + if (!refusal) refusal = "generateKeyPairSync for this key type"; + } else if (sp_is_str(type, "ec")) { + if (!sp_check_object(options, "options")) return false; + gen->kind = SP_GEN_EC; + const ScrDyn *curve_name = sp_opt(options, "namedCurve"); + if (curve_name->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); + return false; + } + const ScrDyn *param = sp_opt(options, "paramEncoding"); + if (sp_is_str(param, "explicit")) { + if (!refusal) refusal = "generateKeyPair with paramEncoding 'explicit'"; + } else if (!sp_absent(param) && param->kind != SCR_DYN_NULL && !sp_is_str(param, "named")) { + sp_property_invalid("options.paramEncoding", param); + return false; + } + gen->curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(curve_name, sp_curves[i].name) || + (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) + gen->curve = &sp_curves[i]; + if (!gen->curve) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return false; + } + } else if (sp_is_str(type, "ed25519")) { + gen->kind = SP_GEN_ED25519; + } else { + bool named = false; + for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) + named = named || sp_is_str(type, sp_other_named_types[i]); + if (!named) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, + received->data); + scr_str_release(received); + return false; + } + if (!refusal) refusal = "generateKeyPairSync for this key type"; } - /* The ed25519 options are optional. */ - if (!sp_is_object(options) && !(ed && sp_absent(options))) { - scr_dyn_arg_type_fail("options", "of type object", options); - return NULL; - } - if (other) { - sp_not_supported("generateKeyPairSync for this key type"); - return NULL; + if (refusal) { + sp_not_supported(refusal); + return false; } + return gen->kind != SP_GEN_ED25519 || sp_ed() != NULL; +} + +/* OpenSSL's error for an RSA modulus length or public exponent it does not + * generate, as Node reports it, or NULL. */ +static const char *sp_rsa_keygen_failure(double bits, double exponent) { + if (bits < 512) return "error:1C8000AB:Provider routines::key size too small"; + if (exponent == 0) return "Key generation job failed"; + if (bits > 2147483647.0) return "error:02000078:rsa routines::key size too small"; + if (exponent == 1 || fmod(exponent, 2) == 0) return "error:020000B2:rsa routines::pub exponent out of range"; + return NULL; +} + +/* Generates the key `gen` describes: NULL with the error pending, and + * *job_error set for the errors generateKeyPair passes to its callback. */ +static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { + *job_error = false; SpKey *k = sp_key_alloc(true); - if (ed) { - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - const ScrPkiEd25519 *ed = sp_ed(); - if (!ed) { - sp_key_release(k); - return NULL; - } + if (gen->kind == SP_GEN_ED25519) { k->ed25519 = true; sp_random(NULL, k->ed_seed, 32); - ed->public_key(k->ed_pub, k->ed_seed); + scr_pki_ed25519->public_key(k->ed_pub, k->ed_seed); return k; } - if (rsa) { - const ScrDyn *bits = sp_opt(options, "modulusLength"); - if (bits->kind != SCR_DYN_NUM) { + if (gen->kind == SP_GEN_RSA) { + const char *failure = sp_rsa_keygen_failure(gen->bits, gen->exponent); + if (failure) { sp_key_release(k); - scr_dyn_prop_type_fail("options.modulusLength", "of type number", bits); + *job_error = true; + scr_pki_throw_fmt(SCR_ERR_ERROR, NULL, "%s", failure); return NULL; } - double b = bits->v.num; - /* validateUint32: an integer first, then the range. */ - bool integer = isfinite(b) && trunc(b) == b; - if (!integer || b < 0 || b > 4294967295.0) { - sp_key_release(k); - ScrStr *received = sp_received(bits, true); - scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", - "The value of \"options.modulusLength\" is out of range. It must be %s. Received %.*s", - integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); - scr_str_release(received); - return NULL; - } - const ScrDyn *exp = sp_opt(options, "publicExponent"); - double e = 65537; - if (!sp_absent(exp)) { - if (exp->kind != SCR_DYN_NUM) { - sp_key_release(k); - scr_dyn_prop_type_fail("options.publicExponent", "of type number", exp); - return NULL; - } - e = exp->v.num; - } - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - if (b < 512 || b > 16384 || e < 3 || e > 2147483647.0 || e != (double)(int64_t)e || ((int64_t)e & 1) == 0 || + /* mbedTLS generates even modulus lengths from 1024 to 8192 bits and, + * past the checks above, odd exponents up to INT_MAX. */ + double b = gen->bits, e = gen->exponent; + if (b < MBEDTLS_RSA_GEN_KEY_MIN_BITS || b > MBEDTLS_MPI_MAX_BITS || fmod(b, 2) != 0 || e > 2147483647.0 || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { sp_key_release(k); @@ -1309,29 +1478,8 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { } return k; } - const ScrDyn *curve_name = sp_opt(options, "namedCurve"); - if (curve_name->kind != SCR_DYN_STR) { - sp_key_release(k); - scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); - return NULL; - } - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - const SpCurve *curve = NULL; - for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) - if (sp_is_str(curve_name, sp_curves[i].name) || - (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) - curve = &sp_curves[i]; - if (!curve) { - sp_key_release(k); - sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); - return NULL; - } if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || - mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + mbedtls_ecp_gen_key(gen->curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { sp_key_release(k); sp_unsupported_input(); return NULL; @@ -1339,17 +1487,14 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { return k; } -/* generateKeyPairSync(type, options): { publicKey, privateKey }, each a - * KeyObject or the encoding the options ask for. */ -ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { - if (options->kind == SCR_DYN_TYPED_REF) { - ScrDyn *value = scr_dyn_typed_ref_materialize(options); - if (!value) return NULL; - ScrDyn *result = scr_crypto_generate_key_pair_sync(type, value); - scr_dyn_release(value); - return result; - } - SpKey *k = sp_generate(type, options); +/* The { publicKey, privateKey } pair for (type, options), each a + * KeyObject or the encoding the options ask for; NULL with the error + * pending, and *job_error as sp_keygen_run sets it. */ +static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_error) { + *job_error = false; + SpKeyGen gen; + if (!sp_keygen_check(type, options, &gen)) return NULL; + SpKey *k = sp_keygen_run(&gen, job_error); if (!k) return NULL; SpKey *pub = sp_public_of(k); if (!pub) { @@ -1373,6 +1518,16 @@ ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { return pair; } +/* generateKeyPairSync(type, options). */ +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + bool job_error; + ScrDyn *pair = sp_key_pair(type, plain, &job_error); + scr_dyn_release(plain); + return pair; +} + /* ── sign and verify ──────────────────────────────────────────────────── */ typedef struct { @@ -1931,12 +2086,44 @@ static void sp_key_pair_deliver(ScrClosure *self) { scr_dyn_release(callback); } -/* generateKeyPair(type, options, callback): argument errors throw, and the - * callback receives (null, publicKey, privateKey) in a later macrotask. */ +/* Calls the callback with the error alone, as Node does for a key + * generation that fails. */ +static void sp_key_pair_fail(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *error = sp_cap(self, 1); + ScrDyn *result = scr_dyn_call(callback, &error, 1, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(error); + scr_dyn_release(callback); +} + +/* generateKeyPair(type, options, callback): a function in the options + * position is the callback. Argument errors throw; the callback receives + * (null, publicKey, privateKey), or the error of a key generation that + * fails, in a later macrotask. */ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { - ScrDyn *pair = scr_crypto_generate_key_pair_sync(type, options); - if (!pair) return; - ScrClosure *c = scr_closure_new((void *)sp_key_pair_deliver, 2); + if (options->kind == SCR_DYN_FUNC) { + callback = options; + options = scr_dyn_undefined(); + } + if (callback->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + return; + } + ScrDyn *plain = sp_plain(options); + if (!plain) return; + bool job_error; + ScrDyn *pair = sp_key_pair(type, plain, &job_error); + scr_dyn_release(plain); + if (!pair && !job_error) return; + void *deliver = (void *)sp_key_pair_deliver; + if (!pair) { + ScrCaught *caught = scr_exc_take(); + pair = 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); diff --git a/tests/corpus/7445-crypto-key-pair-options.mjs b/tests/corpus/7445-crypto-key-pair-options.mjs new file mode 100644 index 000000000..86138e860 --- /dev/null +++ b/tests/corpus/7445-crypto-key-pair-options.mjs @@ -0,0 +1,105 @@ +// generateKeyPairSync and generateKeyPair check their arguments in Node's +// order: the callback, the type, the options object, the public and then +// the private key encoding, and then the options of the key type. The +// encodings follow Node's parseKeyEncoding rules for formats, types, +// ciphers and passphrases. +import * as crypto from "node:crypto"; + +function attempt(label, run) { + try { + const result = run(); + console.log(label, result === undefined ? "returned" : Object.keys(result)); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +const ec = (options) => () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", ...options }); +const rsa = (options) => () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, ...options }); + +// The type, then the options object, then the encodings, then the rest. +attempt("number type", () => crypto.generateKeyPairSync(5, 7)); +attempt("unknown type, bad encoding", () => crypto.generateKeyPairSync("foo", { publicKeyEncoding: 5 })); +attempt("unknown type, number options", () => crypto.generateKeyPairSync("foo", 5)); +attempt("unknown type, null options", () => crypto.generateKeyPairSync("foo", null)); +attempt("unknown type, array options", () => crypto.generateKeyPairSync("foo", [])); +attempt("unknown type", () => crypto.generateKeyPairSync("foo")); +attempt("upper-case types", () => crypto.generateKeyPairSync("RSA", {})); +attempt("upper-case Ed25519", () => crypto.generateKeyPairSync("Ed25519")); +attempt("upper-case ML-DSA", () => crypto.generateKeyPairSync("ML-DSA-44")); +attempt("unknown type, pkcs1", () => crypto.generateKeyPairSync("foo", { publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("rsa, bad modulusLength and encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: "x", publicKeyEncoding: 5 })); +attempt("rsa, bad modulusLength and private encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: "x", privateKeyEncoding: 5 })); +attempt("rsa, negative modulusLength and format", () => crypto.generateKeyPairSync("rsa", { modulusLength: -1, publicKeyEncoding: { format: "x" } })); +attempt("rsa, bad exponent and encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicExponent: "x", publicKeyEncoding: 5 })); +attempt("rsa-pss, bad encoding", () => crypto.generateKeyPairSync("rsa-pss", { publicKeyEncoding: 5 })); +attempt("rsa-pss, bad modulusLength", () => crypto.generateKeyPairSync("rsa-pss", { modulusLength: "x" })); +attempt("dsa, bad encoding", () => crypto.generateKeyPairSync("dsa", { publicKeyEncoding: 5 })); +attempt("dsa, no options", () => crypto.generateKeyPairSync("dsa")); +attempt("dh, number options", () => crypto.generateKeyPairSync("dh", 5)); +attempt("ec, no curve and bad encoding", () => crypto.generateKeyPairSync("ec", { publicKeyEncoding: 5 })); +attempt("ec, unknown curve and bad encoding", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope", publicKeyEncoding: 5 })); +attempt("ec, number curve", () => crypto.generateKeyPairSync("ec", { namedCurve: 5 })); +attempt("ec, lower-case NIST name", () => crypto.generateKeyPairSync("ec", { namedCurve: "p-256" })); +attempt("ec, unknown curve and paramEncoding", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope", paramEncoding: "bogus" })); +attempt("ec, paramEncoding bogus", ec({ paramEncoding: "bogus" })); +attempt("ec, paramEncoding named", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: "named" })); +attempt("ec, paramEncoding null", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: null })); +attempt("ed25519, no options", () => crypto.generateKeyPairSync("ed25519")); +attempt("ed25519, null options", () => crypto.generateKeyPairSync("ed25519", null)); +attempt("ed25519, number options", () => crypto.generateKeyPairSync("ed25519", 5)); +attempt("ed25519, bad encoding", () => crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: 5 })); + +// Encodings. +attempt("both encodings invalid", rsa({ publicKeyEncoding: { format: "x" }, privateKeyEncoding: 5 })); +attempt("array encoding", rsa({ publicKeyEncoding: [] })); +attempt("null encodings", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: null, privateKeyEncoding: null })); +attempt("der without type", rsa({ publicKeyEncoding: { format: "der" } })); +attempt("pem without type", rsa({ privateKeyEncoding: { format: "pem" } })); +attempt("jwk without type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { format: "jwk" }, privateKeyEncoding: { format: "jwk" } })); +attempt("jwk with bad type", rsa({ publicKeyEncoding: { format: "jwk", type: "bogus" } })); +attempt("jwk with pkcs1 for ec", ec({ publicKeyEncoding: { format: "jwk", type: "pkcs1" } })); +attempt("public sec1", rsa({ publicKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("public pkcs8", rsa({ publicKeyEncoding: { format: "pem", type: "pkcs8" } })); +attempt("private spki", rsa({ privateKeyEncoding: { format: "pem", type: "spki" } })); +attempt("private sec1 for rsa", rsa({ privateKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("public pkcs1 for ec", ec({ publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("private pkcs1 for ec", ec({ privateKeyEncoding: { format: "der", type: "pkcs1" } })); +attempt("public pkcs1 for ed25519", () => crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("private sec1 for ed25519", () => crypto.generateKeyPairSync("ed25519", { privateKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("raw-public as private", ec({ privateKeyEncoding: { format: "raw-public" } })); +attempt("raw-private as public", ec({ publicKeyEncoding: { format: "raw-private" } })); +attempt("raw-seed as public", ec({ publicKeyEncoding: { format: "raw-seed" } })); +attempt("raw-public with bad type", ec({ publicKeyEncoding: { format: "raw-public", type: "x" } })); +attempt("raw-private with a type", ec({ privateKeyEncoding: { format: "raw-private", type: "pkcs8" } })); +attempt("raw-private with a passphrase", ec({ privateKeyEncoding: { format: "raw-private", passphrase: "x" } })); +attempt("raw-seed with a cipher", ec({ privateKeyEncoding: { format: "raw-seed", cipher: "aes-128-cbc" } })); +attempt("number cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: 5 } })); +attempt("passphrase without cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", passphrase: "x" } })); +attempt("passphrase with null cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: null, passphrase: "x" } })); +attempt("der pkcs1 with a cipher", rsa({ privateKeyEncoding: { format: "der", type: "pkcs1", cipher: "aes-128-cbc", passphrase: "x" } })); +attempt("der sec1 with a cipher", ec({ privateKeyEncoding: { format: "der", type: "sec1", cipher: "aes-128-cbc", passphrase: "x" } })); +attempt("number passphrase", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc", passphrase: 5 } })); +attempt("missing passphrase", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc" } })); +attempt("encoding errors before the modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1.5, privateKeyEncoding: { format: "pem", type: "pkcs8", passphrase: "x" } })); + +// generateKeyPair checks its callback first, reads a function in the +// options position as the callback, and throws argument errors. +attempt("async, options as the callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 })); +attempt("async, undefined callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 }, undefined)); +attempt("async, number callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 }, 5)); +attempt("async, number type without callback", () => crypto.generateKeyPair(5, {})); +attempt("async, unknown type", () => crypto.generateKeyPair("foo", undefined, () => console.log("never"))); +attempt("async, ec without options", () => crypto.generateKeyPair("ec", () => console.log("never"))); +attempt("async, bad encoding", () => crypto.generateKeyPair("ec", { namedCurve: "P-256", publicKeyEncoding: { format: "pem", type: "pkcs1" } }, () => console.log("never"))); +attempt("async, shifted callback", () => + crypto.generateKeyPair("ed25519", (err, publicKey, privateKey) => { + console.log("shifted callback", err, publicKey.asymmetricKeyType, privateKey.type); + crypto.generateKeyPair( + "ed25519", + (err2, publicKey2) => console.log("options callback", err2, publicKey2.type), + () => console.log("never"), + ); + }), +); +console.log("returned"); diff --git a/tests/corpus/7446-crypto-rsa-key-pair-errors.ts b/tests/corpus/7446-crypto-rsa-key-pair-errors.ts new file mode 100644 index 000000000..ca5416bcd --- /dev/null +++ b/tests/corpus/7446-crypto-rsa-key-pair-errors.ts @@ -0,0 +1,72 @@ +// RSA key generation parameters: publicExponent's validateUint32 checks +// and its 65537 default, and the errors OpenSSL reports for modulus +// lengths and exponents it does not generate, in OpenSSL's order. +// generateKeyPairSync throws those errors and generateKeyPair passes them +// to its callback, alone, in a later macrotask. +import * as crypto from "node:crypto"; + +type RsaOptions = { modulusLength: number; publicExponent?: unknown }; + +function attempt(label: string, options: RsaOptions): void { + try { + const { publicKey } = crypto.generateKeyPairSync("rsa", options as crypto.RSAKeyPairKeyObjectOptions); + const details = publicKey.asymmetricKeyDetails!; + console.log(label, details.modulusLength, details.publicExponent); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log(label, "throws", error.name, error.code, error.message); + } +} + +attempt("exponent 1.5", { modulusLength: 1024, publicExponent: 1.5 }); +attempt("exponent NaN", { modulusLength: 1024, publicExponent: NaN }); +attempt("exponent Infinity", { modulusLength: 1024, publicExponent: Infinity }); +attempt("exponent -1", { modulusLength: 1024, publicExponent: -1 }); +attempt("exponent 2 ** 32", { modulusLength: 1024, publicExponent: 2 ** 32 }); +attempt("exponent string", { modulusLength: 1024, publicExponent: "3" }); +attempt("exponent bigint", { modulusLength: 1024, publicExponent: 3n }); +attempt("exponent null", { modulusLength: 1024, publicExponent: null }); +attempt("exponent undefined", { modulusLength: 1024, publicExponent: undefined }); +attempt("exponent 3", { modulusLength: 1024, publicExponent: 3 }); +attempt("exponent 17", { modulusLength: 1024, publicExponent: 17 }); +attempt("exponent 0", { modulusLength: 1024, publicExponent: 0 }); +attempt("exponent -0", { modulusLength: 1024, publicExponent: -0 }); +attempt("exponent 1", { modulusLength: 1024, publicExponent: 1 }); +attempt("exponent 2", { modulusLength: 1024, publicExponent: 2 }); +attempt("exponent 4", { modulusLength: 1024, publicExponent: 4 }); +attempt("exponent 2 ** 31", { modulusLength: 1024, publicExponent: 2 ** 31 }); +attempt("modulusLength 0", { modulusLength: 0 }); +attempt("modulusLength -0", { modulusLength: -0 }); +attempt("modulusLength 100", { modulusLength: 100 }); +attempt("modulusLength 511", { modulusLength: 511 }); +attempt("modulusLength 2 ** 31", { modulusLength: 2 ** 31 }); +attempt("modulusLength 2 ** 32 - 1", { modulusLength: 2 ** 32 - 1 }); +attempt("modulusLength 100, exponent 4", { modulusLength: 100, publicExponent: 4 }); +attempt("modulusLength 100, exponent 0", { modulusLength: 100, publicExponent: 0 }); +attempt("modulusLength 2 ** 31, exponent 0", { modulusLength: 2 ** 31, publicExponent: 0 }); +attempt("modulusLength 2 ** 31, exponent 4", { modulusLength: 2 ** 31, publicExponent: 4 }); + +function later(label: string, options: RsaOptions, next: () => void): void { + try { + crypto.generateKeyPair("rsa", options as crypto.RSAKeyPairKeyObjectOptions, (...args: unknown[]) => { + const error = args[0] as Error | null; + console.log(label, "callback", args.length, error === null ? null : [error.name, error.message]); + next(); + }); + console.log(label, "returned"); + } catch (e) { + console.log(label, "throws", (e as Error).message); + next(); + } +} + +later("async exponent 4", { modulusLength: 1024, publicExponent: 4 }, () => + later("async modulusLength 100", { modulusLength: 100 }, () => + later("async exponent 0", { modulusLength: 1024, publicExponent: 0 }, () => + later("async exponent 1.5", { modulusLength: 1024, publicExponent: 1.5 }, () => + later("async exponent 3", { modulusLength: 1024, publicExponent: 3 }, () => console.log("done")), + ), + ), + ), +); +process.nextTick(() => console.log("tick")); diff --git a/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json b/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json new file mode 100644 index 000000000..273d0c784 --- /dev/null +++ b/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json @@ -0,0 +1 @@ +{ "plain": 3596, "san": 5499 } diff --git a/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json b/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json new file mode 100644 index 000000000..ed5d25bcc --- /dev/null +++ b/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 7539, "san": 19613 } From 945a519036530065664e07e85754ff3a7dd5cc6f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:49:51 -0500 Subject: [PATCH 155/169] Match Node for typed pipeline callbacks and rl.cursor on non-terminal interfaces --- .../builtin-apis/streams-and-readline.mdx | 4 +- .../frontend/lowering/builtins/readline.ts | 51 +++++++- .../lowering/expressions/narrowing.ts | 3 + .../src/frontend/lowering/lower-stream.ts | 24 +++- .../compiler/src/frontend/lowering/lowerer.ts | 3 +- .../7500-pipeline-callback-undefined.ts.json | 6 + .../7501-pipeline-callback-undefined.mjs.json | 6 + .../7502-readline-cursor-undefined.ts.json | 6 + .../7500-pipeline-callback-undefined.ts | 120 ++++++++++++++++++ .../7501-pipeline-callback-undefined.mjs | 22 ++++ .../corpus/7502-readline-cursor-undefined.ts | 28 ++++ .../7500-pipeline-callback-undefined.ts.json | 1 + .../7501-pipeline-callback-undefined.mjs.json | 1 + .../7502-readline-cursor-undefined.ts.json | 1 + 14 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json create mode 100644 tests/corpus/7500-pipeline-callback-undefined.ts create mode 100644 tests/corpus/7501-pipeline-callback-undefined.mjs create mode 100644 tests/corpus/7502-readline-cursor-undefined.ts create mode 100644 tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json create mode 100644 tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json create mode 100644 tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 166fc3402..45a784742 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -18,7 +18,7 @@ Stages may take the `{ signal }` argument. With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`. `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`). - In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error. + In typed code, a pipeline callback with an async function or async generator destination receives `null` for no error when its error parameter cannot hold `undefined` (@types/node declares it `NodeJS.ErrnoException | null`), and `undefined` when it can, as in Node. `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options. Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream. @@ -43,7 +43,7 @@ The output's `'resize'` event does not redraw the line. The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end. In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself. - `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`. + `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node; `rl.cursor` is `undefined` on an interface that is not a terminal. `rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node. In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported. In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`. diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 846b9a926..aca209017 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -6,9 +6,11 @@ import { DYN, F64, type IrExpr, + type IrType, type IrStmt, type SrcLoc, STRING, + UNDEFINED_T, VOID, arrayOf, BYTES_U8, @@ -54,6 +56,21 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean }); } +/** `rl.cursor`: @types/node declares a number, but an interface that is + * not a terminal answers undefined, so the read keeps its + * `number | undefined` union. */ +export function isOptionalReadlineCursorRead( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + return ( + expr.name.text === "cursor" && + !expr.questionDotToken && + isReadlineTyped(lowerer, expr.expression) && + lowerer.isStdlibMember(expr) + ); +} + /** The runtime's listener event numbers (scr_readline.c). */ const RL_EVENTS: Record = { line: 0, @@ -1341,7 +1358,7 @@ export function lowerReadlineCreateCall( /** Property reads on an Interface: `history` (the history option itself, * or the interface's own empty array), `historySize`, `historyIndex`, * `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `terminal` and - * `cursor` (a non-terminal interface's undefined cursor throws SC2020). + * `cursor` (undefined on a non-terminal interface). * Null for other receivers. */ export function lowerReadlineProperty( lowerer: Lowerer, @@ -1373,10 +1390,38 @@ export function lowerReadlineProperty( loc, }; } - const reads: Record = { + if (name === "cursor") { + // `number | undefined`: a non-terminal interface has no cursor + const recv = lowerer.lowerExprExpecting(access.expression, F64); + const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) }; + const local = recv.kind === "varRef" ? null : lowerer.declareHiddenLocal("%rlCursor", F64); + const id: IrExpr = local ? { kind: "varRef", localId: local.id, type: F64, loc } : recv; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "libCall", fn: "rl.isTerminal", args: [id], type: BOOL, loc }, + then: lowerer.coerceToExpected( + { kind: "libCall", fn: "rl.cursor", args: [id], type: F64, loc }, + type, + ), + else_: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ), + type, + loc, + }; + if (!local) return result; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: recv, loc }], + result, + type, + loc, + }; + } + const reads: Record = { line: ["rl.line", STRING], terminal: ["rl.isTerminal", BOOL], - cursor: ["rl.cursor", F64], }; const read = reads[name]; if (read !== undefined) { diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index fa8ad686f..01cab9901 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -33,6 +33,7 @@ import { isOptionalProcessVersionsRead, } from "../builtins/process.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; +import { isOptionalReadlineCursorRead } from "../builtins/readline.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; import { checkedArmExtraction } from "../coercions/unions.js"; @@ -617,6 +618,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; if (isOptionalProcessVersionsRead(lowerer, node)) return expr; + if (ts.isPropertyAccessExpression(node) && isOptionalReadlineCursorRead(lowerer, node)) + return expr; // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index cf9f19b9d..687418350 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -2906,6 +2906,22 @@ class StreamArgTypeThrow extends Error { } } +/** Whether a checked-dynamic pipeline callback can take undefined as its + * error: a callback that is already checked-dynamic, one without + * parameters, and one whose first parameter (or rest element) type holds + * undefined. */ +function callbackErrorAdmitsUndefined(lowerer: Lowerer, cb: IrExpr): boolean { + if (cb.kind !== "dynFrom" || cb.value.type.kind !== "func") return true; + const fnT = cb.value.type; + const first = fnT.params[0]; + if (first === undefined) return true; + const p0 = + fnT.rest === true && fnT.params.length === 1 && first.kind === "array" ? first.elem : first; + if (p0.kind === "dyn" || p0.kind === "jsval" || p0.kind === "undefinedT") return true; + if (p0.kind !== "union") return false; + return lowerer.unions.get(p0.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false; +} + /** The finished/pipeline completion callback: an inline function lowers * through the option-callback machinery (leading `this`, an `Error | * null` prefix — Node binds the stream and passes the error); any other @@ -3367,12 +3383,16 @@ export function lowerStreamModuleCall( // an async generator) reaches the callback through the // checked-dynamic path. const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); - // SCR_PIPE_TYPED_CB: typed code's callback takes null for no error + // SCR_PIPE_TYPED_CB: a typed callback whose error parameter cannot + // hold undefined takes null for no error; others take undefined. const stagedArgs = [...staged.args]; const flagsArg = stagedArgs[3]!; if (flagsArg.kind !== "numLit") throw new InternalCompilerError("pipeline flags not a literal"); - if (!isJsSourceFile(ts.getSourceFileOfNode(call))) + if ( + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + !callbackErrorAdmitsUndefined(lowerer, cb) + ) stagedArgs[3] = numLit(flagsArg.value | 128, loc); return staged.wrap({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a765c91ee..a17522333 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -496,7 +496,7 @@ import { lowerBuiltinExtraProperty } from "./builtins/properties.js"; import { registerPromisifiedBuiltinDecl } from "./builtins/callable-aliases.js"; import { lowerExecFileAsyncCall } from "./builtins/child-process-async.js"; import { lowerStringDecoderMethodCall, lowerTextCodecCall } from "./builtins/text-codecs.js"; -import { lowerReadlineMethodCall } from "./builtins/readline.js"; +import { isOptionalReadlineCursorRead, lowerReadlineMethodCall } from "./builtins/readline.js"; import { lowerDiagnosticsChannelMethodCall, lowerDiagnosticsChannelProperty, @@ -5547,6 +5547,7 @@ export class Lowerer { if (isIndexedRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true; + if (ts.isPropertyAccessExpression(e) && isOptionalReadlineCursorRead(this, e)) return true; if (isOptionalProcessVersionsRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this)) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json new file mode 100644 index 000000000..fe65b63f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7500-pipeline-callback-undefined.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json new file mode 100644 index 000000000..8d52980fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7501-pipeline-callback-undefined.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json new file mode 100644 index 000000000..6fd23042e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7502-readline-cursor-undefined.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7500-pipeline-callback-undefined.ts b/tests/corpus/7500-pipeline-callback-undefined.ts new file mode 100644 index 000000000..e1503ff6b --- /dev/null +++ b/tests/corpus/7500-pipeline-callback-undefined.ts @@ -0,0 +1,120 @@ +// pipeline() with an async function or async generator destination: a +// typed callback whose error parameter admits undefined receives +// undefined when the pipeline succeeds, as in Node, and the error when it +// fails. +import { pipeline, Readable } from "node:stream"; + +function source(): Readable { + return Readable.from(["a", "b", "c"]); +} + +async function join(src: AsyncIterable): Promise { + let s = ""; + for await (const c of src) s += c; + return s; +} + +function optional(): Promise { + return new Promise((resolve) => { + pipeline(source(), join, (err?: Error | null, value?: string) => { + console.log("optional", err, err === undefined, value); + resolve(); + }); + }); +} + +function declared(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function (src: AsyncIterable) { + let n = 0; + for await (const c of src) n += c.length; + return n; + }, + (err: Error | null | undefined, value?: number) => { + console.log("declared", err, typeof err, value); + resolve(); + }, + ); + }); +} + +function unknownParam(): Promise { + return new Promise((resolve) => { + pipeline(source(), join, (err: unknown, value: unknown) => { + console.log("unknown", err, value); + resolve(); + }); + }); +} + +function rest(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function* (src: AsyncIterable) { + for await (const c of src) yield c.toUpperCase(); + }, + async function (src: AsyncIterable) { + for await (const c of src) void c; + }, + (...args: unknown[]) => { + console.log("rest", args.length, args); + resolve(); + }, + ); + }); +} + +function generatorDestination(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function* (src: AsyncIterable) { + for await (const c of src) yield c; + }, + (err: Error | null | undefined, value: unknown) => { + console.log("generator", err, value); + resolve(); + }, + ); + }); +} + +function named(): Promise { + return new Promise((resolve) => { + function done(err: Error | null | undefined, value: string | undefined): void { + console.log("named", err, value); + resolve(); + } + pipeline(source(), join, done); + }); +} + +function failing(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function (src: AsyncIterable) { + for await (const c of src) if (c === "b") throw new Error("boom at " + c); + return "never"; + }, + (err?: Error | null, value?: string) => { + console.log("failing", err?.message, value); + resolve(); + }, + ); + }); +} + +async function main(): Promise { + await optional(); + await declared(); + await unknownParam(); + await rest(); + await generatorDestination(); + await named(); + await failing(); +} +void main(); diff --git a/tests/corpus/7501-pipeline-callback-undefined.mjs b/tests/corpus/7501-pipeline-callback-undefined.mjs new file mode 100644 index 000000000..c51738e77 --- /dev/null +++ b/tests/corpus/7501-pipeline-callback-undefined.mjs @@ -0,0 +1,22 @@ +// pipeline() with an async function destination in JavaScript: the +// callback receives undefined and the destination's value on success. +import { pipeline, Readable } from "node:stream"; + +pipeline( + Readable.from(["x", "y"]), + async function (src) { + const out = []; + for await (const c of src) out.push(c); + return out.join("-"); + }, + (err, value) => { + console.log("callback", err, typeof err, value); + pipeline( + Readable.from(["z"]), + async function (src) { + for await (const c of src) void c; + }, + (...args) => console.log("no value", args), + ); + }, +); diff --git a/tests/corpus/7502-readline-cursor-undefined.ts b/tests/corpus/7502-readline-cursor-undefined.ts new file mode 100644 index 000000000..abac985cb --- /dev/null +++ b/tests/corpus/7502-readline-cursor-undefined.ts @@ -0,0 +1,28 @@ +// rl.cursor answers undefined on an interface that is not a terminal and +// the cursor position on a terminal interface. +import * as readline from "node:readline"; +import { PassThrough, Writable } from "node:stream"; + +const input = new PassThrough(); +const rl = readline.createInterface({ input }); +const c = rl.cursor; +console.log("cursor", c, typeof rl.cursor, rl.cursor === undefined); +console.log("arithmetic", rl.cursor + 1, Number.isNaN(rl.cursor * 2)); +console.log("fallback", rl.cursor ?? -1, `${rl.cursor}`); + +const interfaces = [rl]; +function first(): readline.Interface { + return interfaces[0]!; +} +console.log("call receiver", first().cursor); +rl.close(); + +const sink = new Writable({ + write(_chunk: Buffer, _enc, cb) { + cb(); + }, +}); +const terminal = readline.createInterface({ input: new PassThrough(), output: sink, terminal: true }); +terminal.write("abc"); +console.log("terminal", terminal.cursor, typeof terminal.cursor, terminal.cursor + 1); +terminal.close(); diff --git a/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json b/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json new file mode 100644 index 000000000..122c66b27 --- /dev/null +++ b/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 3783, "san": 4994 } diff --git a/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json b/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json new file mode 100644 index 000000000..d9c00a7de --- /dev/null +++ b/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json @@ -0,0 +1 @@ +{ "plain": 2457, "san": 3243 } diff --git a/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json b/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json new file mode 100644 index 000000000..7a36db8f3 --- /dev/null +++ b/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 2592, "san": 3421 } From fb200c1240de049c2b0ec56540e27437d8f8ead9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:27:14 -0500 Subject: [PATCH 156/169] Reuse the bundled declaration files' diagnostics across builds --- .../src/frontend/ts7/lib-diagnostics.test.ts | 221 ++++++++++++ .../src/frontend/ts7/lib-diagnostics.ts | 324 ++++++++++++++++++ .../compiler/src/frontend/ts7/program-host.ts | 35 +- packages/compiler/src/frontend/ts7/session.ts | 30 ++ packages/compiler/src/index.ts | 21 ++ packages/compiler/src/native/cli.ts | 17 +- 6 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts create mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.ts diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts new file mode 100644 index 000000000..f041a6cb5 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts @@ -0,0 +1,221 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, afterEach, expect, test } from "vitest"; +import type { SourceFile } from "./ast-types.js"; +import { + reusedLibDiagnostics, + setLibDiagnosticsStore, + type LibDiagnosticsStore, +} from "./lib-diagnostics.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js"; +import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); +const lib = join(dir, "lib.es5.d.ts"); +const fallback = join(dir, "scriptc-node-fallback.d.ts"); +const libText = "declare var NaN: number;\ninterface Array { length: number }\n"; +const fallbackText = "export {};\ndeclare global {\n var process: { argv: string[] };\n}\n"; +writeFileSync(lib, libText); +writeFileSync(fallback, fallbackText); +const main = join(dir, "main.ts"); +const util = join(dir, "\u{1F600}.ts"); +const other = join(dir, "�.ts"); + +afterEach(() => setLibDiagnosticsStore(null)); +afterAll(() => { + closeSourceParser(); + rmSync(dir, { recursive: true, force: true }); +}); + +function memoryStore(): LibDiagnosticsStore & { entries: Map } { + const entries = new Map(); + return { + entries, + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; +} + +function diagnostic(fileName: string | undefined, pos: number, code = 2322): Ts7DiagnosticData { + return { fileName, pos, end: pos + 1, code, category: 1, text: `at ${pos}` }; +} + +/** A session program whose full-check and checked-file answers are given; it + * records which requests it was asked. */ +function sessionProgram(answers: { + semantic: Map; + global?: Ts7DiagnosticData[]; + checked?: boolean; +}): Ts7SessionProgram & { asked: string[] } { + const asked: string[] = []; + const all = [...answers.semantic.values()].flat(); + return { + asked, + getCompilerOptions: () => ({ strict: true }), + diagnosticData: (method: string, file?: string) => { + asked.push(file === undefined ? method : `${method} ${file}`); + if (method === "getGlobalDiagnostics") return answers.global ?? []; + return file === undefined ? all : (answers.semantic.get(file) ?? []); + }, + checkedFileDiagnosticData: (files: readonly string[]): Ts7CheckedFileDiagnosticData | null => { + asked.push(`checked ${files.join(",")}`); + if (answers.checked === false) return null; + return { files: files.map((file) => answers.semantic.get(file) ?? []), global: [] }; + }, + } as unknown as Ts7SessionProgram & { asked: string[] }; +} + +/** The program's files, parsed: the supplied two and the given others. */ +function files(texts: Record): (name: string) => SourceFile | undefined { + const parsed = new Map([ + [lib, parseSourceFile(lib, libText, "ts")], + [fallback, parseSourceFile(fallback, fallbackText, "ts")], + ]); + for (const [name, text] of Object.entries(texts)) + parsed.set(name, parseSourceFile(name, text, "ts")); + return (name) => parsed.get(name); +} + +const names = [lib, fallback, main]; +const supplied = (name: string): boolean => name === lib || name === fallback; +const moduleFile = files({ [main]: "export const n: number = 'x';\n" }); + +test("without a store the full check answers", () => { + const program = sessionProgram({ semantic: new Map() }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toBeNull(); + expect(program.asked).toEqual([]); +}); + +test("a stored answer stands in for checking the supplied files, merged by file name", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([ + [lib, [diagnostic(lib, 1)]], + [fallback, []], + [main, [diagnostic(main, 5), diagnostic(main, 9)]], + ]); + const first = sessionProgram({ semantic }); + const full = reusedLibDiagnostics(first, names, supplied, moduleFile); + // The first build runs the full check and stores the supplied files' answers. + expect(first.asked).toEqual([ + "getGlobalDiagnostics", + "getSemanticDiagnostics", + "checked ", + `getSemanticDiagnostics ${lib}`, + `getSemanticDiagnostics ${fallback}`, + ]); + expect(store.entries.size).toBe(1); + const second = sessionProgram({ semantic }); + const reused = reusedLibDiagnostics(second, names, supplied, moduleFile); + expect(second.asked).toEqual([`checked ${main}`]); + // The files' diagnostics in the order of their names (lib.es5.d.ts, main.ts), as the full answer. + expect(reused).toEqual({ + semantic: [diagnostic(lib, 1), diagnostic(main, 5), diagnostic(main, 9)], + global: [], + }); + expect(full?.semantic).toEqual(reused?.semantic); +}); + +test("files merge in code point order, which differs from UTF-16 order", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const all = [lib, fallback, util, other]; + const sources = files({ [util]: "export {};\n", [other]: "export {};\n" }); + const semantic = new Map([ + [util, [diagnostic(util, 1)]], + [other, [diagnostic(other, 2)]], + ]); + reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + const reused = reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + // U+FFFD sorts before U+1F600 by code point (its surrogates sort after it by UTF-16 unit). + expect(reused?.semantic).toEqual([diagnostic(other, 2), diagnostic(util, 1)]); +}); + +test("a full check with global diagnostics is not stored", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ + semantic: new Map([[main, [diagnostic(main, 1)]]]), + global: [diagnostic(undefined, 0, 2318)], + }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)?.global).toEqual([ + diagnostic(undefined, 0, 2318), + ]); + expect(store.entries.size).toBe(0); +}); + +test("scripts reuse the answer unless they declare a global the supplied files declare", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + // A script declaring only new names: it stores the answer, and a later one reuses it. + const fresh = files({ + [main]: "const total = 1;\nfunction run() {}\nif (total) { var later = 2; }\n", + }); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, fresh); + expect(store.entries.size).toBe(1); + const again = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(again, names, supplied, fresh)).not.toBeNull(); + expect(again.asked).toEqual([`checked ${main}`]); + // Scripts declaring a supplied global, at the top level, in a block, in a + // `declare global` of a supplied module, or an ambient module: the full check. + for (const text of [ + "interface Array { extra: T }\n", + "if (true) { var NaN = 1; }\n", + "declare var process: unknown;\n", + 'declare module "fs" {}\n', + ]) { + const program = sessionProgram({ semantic }); + const result = reusedLibDiagnostics(program, names, supplied, files({ [main]: text })); + expect(program.asked[0], text).not.toBe(`checked ${main}`); + if (result !== null) expect(program.asked[0]).toBe("getGlobalDiagnostics"); + } + expect(store.entries.size).toBe(1); +}); + +test("a program whose script declares a supplied global does not store its answer", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ semantic: new Map([[main, []]]) }); + reusedLibDiagnostics(program, names, supplied, files({ [main]: "declare var NaN: number;\n" })); + expect(store.entries.size).toBe(0); +}); + +test("modules that can declare something global, and servers without the method, ask as before", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + expect(store.entries.size).toBe(1); + for (const text of [ + "export {};\ndeclare global { var extra: number }\n", + 'export {};\ndeclare module "./other" { const extra: number }\n', + "export as namespace Lib;\nexport const n = 1;\n", + ]) { + const program = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(program, names, supplied, files({ [main]: text }))).toBeNull(); + expect(program.asked).toEqual([]); + } + const old = sessionProgram({ semantic, checked: false }); + expect(reusedLibDiagnostics(old, names, supplied, moduleFile)).toBeNull(); + // Nor does a server without the method store an answer it could not use. + const fresh = memoryStore(); + setLibDiagnosticsStore(fresh, "checker"); + reusedLibDiagnostics(sessionProgram({ semantic, checked: false }), names, supplied, moduleFile); + expect(fresh.entries.size).toBe(0); +}); + +test("a supplied file that changes is a different entry", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + writeFileSync(fallback, fallbackText + "// edited\n"); + const program = sessionProgram({ semantic }); + reusedLibDiagnostics(program, names, supplied, moduleFile); + expect(program.asked[0]).toBe("getGlobalDiagnostics"); + expect(store.entries.size).toBe(2); + writeFileSync(fallback, fallbackText); +}); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts new file mode 100644 index 000000000..827b337bd --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts @@ -0,0 +1,324 @@ +/* The semantic diagnostics of the declaration files scriptc supplies (the + * TypeScript libs and scriptc's own declaration files), reused across + * builds. Checking them is most of the full check of a small program. When + * no other file of a program can change what checking those files reports + * (every one is a module without global or module augmentations, or a + * script declaring no global name the supplied files declare), the answer + * depends only on the checker, the compiler options and the files + * themselves, so a stored answer for the same three stands in for checking + * them again: the server checks only the program's other files + * (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the + * order the full check sorts them, by file name. An answer is stored only + * when the full check reports no global diagnostics: the global diagnostics + * of checking the other files are then the full check's. */ +import { createHash } from "node:crypto"; +import { statSync } from "node:fs"; +import type { Node, SourceFile } from "./ast-types.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7SessionProgram } from "./session.js"; +import { NodeFlags, SyntaxKind } from "./enums.js"; +import { walkPreorder } from "./ast.js"; + +/** Key-value storage for reused diagnostics (best effort: failures miss). */ +export interface LibDiagnosticsStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +const SCHEMA = "scriptc.lib-diagnostics.v1"; + +let configured: { store: LibDiagnosticsStore; checker: string } | null = null; + +/** Has later builds of this process reuse the diagnostics of the supplied + * declaration files through `store`, for the checker identified by + * `checker` (its executable's identity), or stop reusing them (null). */ +export function setLibDiagnosticsStore(store: LibDiagnosticsStore | null, checker = ""): void { + configured = store === null ? null : { store, checker }; +} + +/** A checker executable's identity for the stored diagnostics' key: its + * path and file identity, or the path alone when it cannot be read. */ +export function checkerExecutableIdentity(path: string): string { + try { + const info = statSync(path); + return JSON.stringify([path, info.size, info.mtimeMs, info.ino]); + } catch { + return JSON.stringify([path]); + } +} + +/** The program's semantic and global diagnostics as the full check reports + * them, using stored diagnostics for the supplied declaration files; null + * when they cannot be reused for this program (then the caller asks for + * them as before). `supplied` tells whether a program file is one of the + * supplied declaration files. */ +export function reusedLibDiagnostics( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; + const { store, checker } = configured; + const libs: string[] = []; + const own: string[] = []; + for (const name of fileNames) (supplied(name) ? libs : own).push(name); + if (libs.length === 0) return null; + // The global names each of the program's scripts declares; a module that + // declares nothing global contributes none. + const scripts: Set[] = []; + for (const name of own) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator !== undefined) { + if (!declaresNothingGlobal(file)) return null; + continue; + } + const names = isJsFileName(name) ? null : scriptGlobalNames(file); + if (names === null) return null; + scripts.push(names); + } + // A script's globals cannot change what the supplied files report unless + // one of them is also a global the supplied files declare. + const collides = (globals: ReadonlySet): boolean => + scripts.some((names) => [...names].some((name) => globals.has(name))); + // The options without the configuration file's path: it names where the + // project is, which checking the supplied files does not read (the files a + // project's location brings in are part of the key). + const options = { ...program.getCompilerOptions(), configFilePath: undefined }; + const key = entryKey(checker, options, libs); + if (key === null) return null; + const stored = readEntry(store, key, libs); + if (stored !== null && !collides(stored.globals)) { + const checked = program.checkedFileDiagnosticData(own); + if (checked === null) return null; + const byFile = new Map(stored.files); + own.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); + return { semantic: inFileOrder(byFile), global: checked.global }; + } + // The full check, as before. Its answers for the supplied files are stored + // when it reports no global diagnostics and the program's scripts declare + // none of the supplied files' globals (so nothing of the program reached + // what those files report). + const global = program.diagnosticData("getGlobalDiagnostics"); + const semantic = program.diagnosticData("getSemanticDiagnostics"); + // A server that cannot check only some files could never use the answer. + if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) { + const globals = suppliedGlobalNames(libs, sourceFile); + if (globals !== null && !collides(globals)) { + const files: Record = {}; + for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name); + try { + store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files })); + } catch { + // Best effort: a failed write is a later miss. + } + } + } + return { semantic, global }; +} + +/** Whether a file cannot add to or change the global declarations the + * supplied files declare: a module with no `declare global`, no module + * augmentation and no UMD `export as namespace` declaration. */ +function declaresNothingGlobal(file: SourceFile): boolean { + if (file.externalModuleIndicator === undefined) return false; + if (file.moduleAugmentations.length > 0) return false; + for (const statement of file.statements.nodes) { + if (statement.kind === SyntaxKind.NamespaceExportDeclaration) return false; + // `declare global` (a module declaration named by the identifier `global`). + if ( + statement.kind === SyntaxKind.ModuleDeclaration && + statement.name?.kind === SyntaxKind.Identifier && + statement.name.text === "global" + ) + return false; + } + return true; +} + +function isJsFileName(name: string): boolean { + return /\.[cm]?jsx?$/.test(name); +} + +/** The names a script file declares in the global scope: its top-level + * declarations, and the `var` and function declarations of its top-level + * blocks; null when it declares an ambient module (`declare module "x"`), + * which can merge with one the supplied files declare. Over-approximates: + * a name more only makes the stored answer apply less often. */ +function scriptGlobalNames(file: SourceFile): Set | null { + const names = new Set(); + let ambientModule = false; + walkPreorder(file, (node) => { + if (node === file) return undefined; + if (node.kind === SyntaxKind.ModuleDeclaration && node.name?.kind === SyntaxKind.StringLiteral) + ambientModule = true; + const topLevel = node.parent === file; + if (topLevel || node.kind === SyntaxKind.FunctionDeclaration) addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + // Functions, classes and namespaces have scopes of their own. + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + return ambientModule ? null : names; +} + +/** The global names the supplied files declare: the declarations of their + * scripts (as scriptGlobalNames finds them, with ambient modules as their + * quoted names) and those of the `declare global` blocks of their modules; + * null when one of them is not available. */ +function suppliedGlobalNames( + libs: readonly string[], + sourceFile: (fileName: string) => SourceFile | undefined, +): Set | null { + const names = new Set(); + for (const name of libs) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator === undefined) { + walkPreorder(file, (node) => { + if (node === file) return undefined; + if ( + node.kind === SyntaxKind.ModuleDeclaration && + node.name?.kind === SyntaxKind.StringLiteral + ) + names.add(JSON.stringify(node.name.text ?? "")); + if (node.parent === file || node.kind === SyntaxKind.FunctionDeclaration) + addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + continue; + } + for (const statement of file.statements.nodes) { + if ( + statement.kind !== SyntaxKind.ModuleDeclaration || + statement.name?.kind !== SyntaxKind.Identifier || + statement.name.text !== "global" + ) + continue; + for (const inner of statement.body?.statements?.nodes ?? []) addDeclaredNames(inner, names); + } + } + return names; +} + +const GLOBAL_SCOPE_BOUNDARIES = new Set([ + SyntaxKind.FunctionDeclaration, + SyntaxKind.FunctionExpression, + SyntaxKind.ArrowFunction, + SyntaxKind.MethodDeclaration, + SyntaxKind.Constructor, + SyntaxKind.GetAccessor, + SyntaxKind.SetAccessor, + SyntaxKind.ClassDeclaration, + SyntaxKind.ClassExpression, + SyntaxKind.ModuleDeclaration, +]); + +/** The names a statement declares in the scope it is in. */ +function addDeclaredNames(node: Node, names: Set): void { + switch (node.kind) { + case SyntaxKind.VariableStatement: + for (const declaration of node.declarationList?.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + if (node.name?.kind === SyntaxKind.Identifier) names.add(node.name.text ?? ""); + return; + } +} + +function addBindingNames(name: Node | undefined, names: Set): void { + if (name === undefined) return; + if (name.kind === SyntaxKind.Identifier) { + names.add(name.text ?? ""); + return; + } + // Object and array binding patterns. + for (const element of name.elements?.nodes ?? []) addBindingNames(element.name, names); +} + +/** The stored diagnostics' key: the checker, the compiler options and the + * supplied files in program order with each one's file identity; null when + * one of them cannot be read. */ +function entryKey(checker: string, options: unknown, libs: readonly string[]): string | null { + const files: unknown[] = []; + for (const name of libs) { + try { + const info = statSync(name); + files.push([name, info.size, info.mtimeMs, info.ino]); + } catch { + return null; + } + } + return createHash("sha256") + .update(JSON.stringify({ schema: SCHEMA, checker, options, files })) + .digest("hex"); +} + +function readEntry( + store: LibDiagnosticsStore, + key: string, + libs: readonly string[], +): { files: Map; globals: Set } | null { + let text: string | null; + try { + text = store.read(key); + } catch { + return null; + } + if (text === null) return null; + let entry: { schema?: unknown; globals?: unknown; files?: Record }; + try { + entry = JSON.parse(text) as typeof entry; + } catch { + return null; + } + if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals)) + return null; + const files = new Map(); + for (const name of libs) { + const list = entry.files[name]; + if (!Array.isArray(list)) return null; + files.set(name, list); + } + return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) }; +} + +/** Each file's diagnostics (already in the checker's order) in the order the + * checker sorts files: by file name, compared by code point. */ +function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { + const names = [...byFile.keys()].sort(compareCodePoints); + const out: Ts7DiagnosticData[] = []; + for (const name of names) for (const d of byFile.get(name)!) out.push(d); + return out; +} + +function compareCodePoints(a: string, b: string): number { + let i = 0; + let j = 0; + while (i < a.length && j < b.length) { + const x = a.codePointAt(i)!; + const y = b.codePointAt(j)!; + if (x !== y) return x < y ? -1 : 1; + i += x > 0xffff ? 2 : 1; + j += y > 0xffff ? 2 : 1; + } + return i < a.length ? 1 : j < b.length ? -1 : 0; +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 639086e94..ec143e401 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,6 +46,10 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; +import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; +import { reusedLibDiagnostics } from "./lib-diagnostics.js"; +import { ts7Diagnostic } from "./diagnostic.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -393,6 +397,22 @@ export class Ts7Program { return this.project.program.getSourceFileMetadata(resolve(fileName)); } + /** The semantic and global diagnostics with the stored diagnostics of the + * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ + reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + this.snapshot.ensureActive(); + const program = this.project.program; + const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); + return reusedLibDiagnostics( + program, + this.getSourceFileNames(), + (name) => + declarations.has(tsgoPath(name)) || + program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true, + (name) => this.getSourceFile(name), + ); + } + isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } @@ -423,9 +443,20 @@ export class Ts7Program { * package dropped the aggregate). Order mirrors 5.9.3: config, options/ * program, global, syntactic, semantic. */ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { + const config = program.getConfigFileParsingDiagnostics(); + const options = program.getProgramDiagnostics(); + const reused = program.reusedLibDiagnostics(); + if (reused !== null) + return [ + ...config, + ...options, + ...reused.global.map(ts7Diagnostic), + ...program.getSyntacticDiagnostics(), + ...reused.semantic.map(ts7Diagnostic), + ]; return [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getProgramDiagnostics(), + ...config, + ...options, ...program.getGlobalDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(), diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index d3f2ae9fe..082bb79a6 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -55,6 +55,13 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; + files?: string[]; +} + +/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ +export interface Ts7CheckedFileDiagnosticData { + files: Ts7DiagnosticData[][]; + global: Ts7DiagnosticData[]; } /** Platform-independent session over an already connected transport. The @@ -344,6 +351,29 @@ export class Ts7SessionProgram { result.push(ts7Diagnostic(data)); return result; } + /** The diagnostics of `method` for `file`, or for the whole program, as + * the server sends them. */ + diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { + return this.session.request(method, this.query(file)) ?? []; + } + /** The semantic diagnostics of each of `files` and the global diagnostics, + * after the server checks only those files (getDiagnosticsOfCheckedFiles), + * or null when the server does not answer that method. */ + checkedFileDiagnosticData(files: readonly string[]): Ts7CheckedFileDiagnosticData | null { + try { + return this.session.request("getDiagnosticsOfCheckedFiles", { + ...this.query(), + files: [...files], + }); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('unknown API method "getDiagnosticsOfCheckedFiles"') + ) + return null; + throw error; + } + } getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 4f7b93cfd..586561632 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -9,6 +9,7 @@ import { } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; +import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -1161,9 +1162,28 @@ async function prepareExecutableInputInChild( * `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM * text, to `resultPath`. */ +/** Has the build reuse the diagnostics of the declaration files scriptc + * supplies (lib-diagnostics.ts) through the build cache, when there is one and + * the checker is tsc-ts. */ +async function configureLibDiagnostics(): Promise { + let checker: string | undefined; + try { + checker = checkerCacheIdentity(); + } catch { + // A missing checker fails where the build starts it. + checker = undefined; + } + const root = checker === undefined ? null : await prepareBuildCacheRoot(buildCacheRoot()); + setLibDiagnosticsStore( + root === null ? null : fileVerdictStore(join(root, "lib-diagnostics-v1")), + checker ?? "", + ); +} + export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; clearCompileSessionCaches(); + await configureLibDiagnostics(); setProvenanceSources(request.provenance); const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; @@ -1226,6 +1246,7 @@ async function compileTracked( const timing = compilationTiming(); const timingStart = Date.now(); entryPath = resolve(entryPath); + await configureLibDiagnostics(); const outputKind = opts.outputKind ?? "exe"; if ( (opts.backend !== undefined && opts.backend !== "llvm") || diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 21047b929..44d5eb9f0 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -16,7 +16,11 @@ import { createNativeTs7Api } from "../frontend/ts7/native-api.js"; import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js"; import { setCompilerReleaseVersion } from "../library/sidecar.js"; import { NativeCompiler } from "./compiler.js"; -import { openNativeCache } from "./cache.js"; +import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js"; +import { + checkerExecutableIdentity, + setLibDiagnosticsStore, +} from "../frontend/ts7/lib-diagnostics.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; @@ -53,6 +57,17 @@ async function main(): Promise { if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot); setCompilerReleaseVersion(toolchain.compilerVersion); cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden)); + if (cache !== null) { + const store = cache; + setLibDiagnosticsStore( + { + read: (key) => + store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, + write: (key, text) => store.write("lib-diagnostics", key, text), + }, + checkerExecutableIdentity(toolchain.ts7Executable), + ); + } compiler = new NativeCompiler(toolchain, cache); return compiler; }; From 58d3603549065404f6b832c4f16b35d93409c82e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:33:56 -0500 Subject: [PATCH 157/169] Accept a callable Proxy as the generateKeyPair callback generateKeyPair tested its callback, and a function in the options position, by the checked-dynamic kind, so a Proxy whose target is a function threw ERR_INVALID_ARG_TYPE. Node checks typeof, which answers "function" for such a Proxy. Both tests now ask whether the value is callable. --- packages/runtime/src/scr_pki.c | 4 ++-- tests/corpus/7445-crypto-key-pair-options.mjs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 888e0eae9..a1a5e97c2 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -2102,11 +2102,11 @@ static void sp_key_pair_fail(ScrClosure *self) { * (null, publicKey, privateKey), or the error of a key generation that * fails, in a later macrotask. */ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { - if (options->kind == SCR_DYN_FUNC) { + if (scr_dyn_is_callable(options)) { callback = options; options = scr_dyn_undefined(); } - if (callback->kind != SCR_DYN_FUNC) { + if (!scr_dyn_is_callable(callback)) { scr_dyn_arg_type_fail("callback", "of type function", callback); return; } diff --git a/tests/corpus/7445-crypto-key-pair-options.mjs b/tests/corpus/7445-crypto-key-pair-options.mjs index 86138e860..2e8f7e8b3 100644 --- a/tests/corpus/7445-crypto-key-pair-options.mjs +++ b/tests/corpus/7445-crypto-key-pair-options.mjs @@ -97,7 +97,19 @@ attempt("async, shifted callback", () => console.log("shifted callback", err, publicKey.asymmetricKeyType, privateKey.type); crypto.generateKeyPair( "ed25519", - (err2, publicKey2) => console.log("options callback", err2, publicKey2.type), + (err2, publicKey2) => { + console.log("options callback", err2, publicKey2.type); + // A callable Proxy is a function, in either position. + const proxied = new Proxy( + (err3, publicKey3) => { + console.log("proxy callback", err3, publicKey3.type); + crypto.generateKeyPair("ed25519", proxiedOptions); + }, + {}, + ); + const proxiedOptions = new Proxy((err4, publicKey4) => console.log("proxy options callback", err4, publicKey4.type), {}); + crypto.generateKeyPair("ed25519", {}, proxied); + }, () => console.log("never"), ); }), From 72736211b1b36712c305443ff8d61040a366d963 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:55:31 -0700 Subject: [PATCH 158/169] Assign private fields through untyped receivers with logical assignment `other.#x ??= value` (and `||=`, `&&=`) on a receiver JavaScript leaves untyped read and wrote an ordinary property named "#x" instead of the brand-checked private field, so the field kept its old value. Hono's RegExpRouter numbers its parameter nodes with `next.#varIndex ??= context.varIndex++`; without the stored index its regular expression lost every parameter capture, and `c.req.param("id")` answered undefined. Co-Authored-By: Claude Opus 5.5 --- .../expressions/nullish-assignment.ts | 4 +- ...private-logical-assignment-untyped.js.json | 6 ++ ...7738-private-logical-assignment-untyped.js | 56 +++++++++++++++++++ ...private-logical-assignment-untyped.js.json | 1 + 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json create mode 100644 tests/corpus/7738-private-logical-assignment-untyped.js create mode 100644 tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 81b3b9d69..9664f591d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -100,7 +100,9 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp write = (value) => eventAccessorWrite(receiver, name, value, lowerer, loc); } else if (ts.isPropertyAccessExpression(target)) { const probed = tryLowerExpression(lowerer, target.expression); - if (probed?.type.kind === "dyn") { + // `value.#x ??= v` on a checked-dynamic value names the brand-checked + // field (fieldTarget), never a property called "#x". + if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(target.name)) { const receiver = save(probed, "%shortCircuitReceiver"); const key: IrExpr = { kind: "strLit", value: target.name.text, type: STRING, loc }; storage = DYN; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json new file mode 100644 index 000000000..4865d3206 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7738-private-logical-assignment-untyped.js" + ], + "diags": [] +} diff --git a/tests/corpus/7738-private-logical-assignment-untyped.js b/tests/corpus/7738-private-logical-assignment-untyped.js new file mode 100644 index 000000000..b0ceaea86 --- /dev/null +++ b/tests/corpus/7738-private-logical-assignment-untyped.js @@ -0,0 +1,56 @@ +// Logical assignments to a private field of another instance whose type +// JavaScript leaves open, the way a router trie numbers its parameter +// nodes on first use (`next.#varIndex ??= context.varIndex++`). They read +// and write the brand-checked field, and evaluate the right side only +// when they store it. +class Node { + #varIndex; + #label = ""; + #count = 0; + #children = Object.create(null); + + child(key, context) { + let next = this.#children[key]; + if (!next) next = this.#children[key] = new Node(); + next.#varIndex ??= context.varIndex++; + next.#label ||= `node:${key}`; + next.#count &&= next.#count + 1; + next.#count ||= 1; + return next; + } + + describe() { + const kind = typeof this.#varIndex === "number" ? "param" : "static"; + return `${kind} ${this.#varIndex} ${this.#label} ${this.#count}`; + } + + static reset(node, other) { + other.#label &&= ""; + other.#varIndex ??= -1; + return [node.#label, other.#label, other.#varIndex]; + } + + static peek(value) { + return value.#varIndex ??= 99; + } +} + +const context = { varIndex: 0 }; +const root = new Node(); +const a = root.child("a", context); +const b = root.child("b", context); +const again = root.child("a", context); +console.log(a === again, context.varIndex); +console.log(root.describe()); +console.log(a.describe()); +console.log(b.describe()); +console.log(JSON.stringify(Node.reset(root, a))); +console.log(a.describe()); + +for (const value of [{}, null]) { + try { + Node.peek(value); + } catch (error) { + console.log(error instanceof TypeError, error.message); + } +} diff --git a/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json b/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json new file mode 100644 index 000000000..d61b17a5d --- /dev/null +++ b/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 5500, "san": 8800 } From ec833b9a02cf0476e7995778b1716d16d4b0582f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 02:57:19 -0700 Subject: [PATCH 159/169] Record the handle method values fixture's baseline and cost Co-Authored-By: Claude Opus 5.5 --- .../tests/corpus/7740-handle-method-values.js.json | 6 ++++++ .../harness/corpus-costs/7740-handle-method-values.js.json | 1 + 2 files changed, 7 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json create mode 100644 tests/harness/corpus-costs/7740-handle-method-values.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json new file mode 100644 index 000000000..ab7f75639 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7740-handle-method-values.js" + ], + "diags": [] +} diff --git a/tests/harness/corpus-costs/7740-handle-method-values.js.json b/tests/harness/corpus-costs/7740-handle-method-values.js.json new file mode 100644 index 000000000..d18e4dddd --- /dev/null +++ b/tests/harness/corpus-costs/7740-handle-method-values.js.json @@ -0,0 +1 @@ +{ "plain": 9800, "san": 12000 } From 952e01d2ed21c7420c0305ca1d9025e246961750 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:03:12 -0500 Subject: [PATCH 160/169] Stream request bodies in the Node server adapter --- docs/content/docs/dependencies.mdx | 2 +- .../ambient/scriptc-node-fallback.d.ts | 1 + .../compiler/shims/hono-node-server/v2.ts | 167 ++++++++++++++++-- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-server.ts | 5 +- .../compiler/src/ir/builtin-signatures.ts | 3 +- ...287-http-held-request-backpressure.ts.json | 6 + packages/runtime/src/scr_http.c | 44 +++-- packages/runtime/src/scr_net.c | 21 ++- packages/runtime/src/scr_net.h | 1 + .../7287-http-held-request-backpressure.ts | 106 +++++++++++ .../npm/cases/hono-node-server/main.ts | 105 ++++++++++- ...287-http-held-request-backpressure.ts.json | 1 + 13 files changed, 425 insertions(+), 38 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json create mode 100644 tests/corpus/7287-http-held-request-backpressure.ts create mode 100644 tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 3098dcad4..14130b234 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -92,7 +92,7 @@ scriptc implements a few packages itself and compiles that implementation in pla - graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying. - An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there. -`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies are read before the handler runs. Known differences: +`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences: - Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files. - The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b195c3fbb..09a3fdd8e 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5561,6 +5561,7 @@ declare module "http" { readonly trailersDistinct: { [name: string]: string[] | undefined }; readonly rawTrailers: string[]; resume(): void; + pause(): void; destroy(): void; setTimeout(msecs: number, callback?: () => void): this; /* setEncoding('utf8'): 'data' delivers strings (other real encodings diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index 94a9c8d85..c46813c7b 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -3,10 +3,12 @@ // node:http. The published package swaps the global Request and Response // for lightweight objects and reparents prototypes, which only pays off // under V8; this port builds native Request objects from the incoming -// message and writes the handler's Response the way the package's -// listener does (header record, content length for bodies that complete -// within the first reads, streamed bodies, error responses), so a client -// sees the same status lines, headers and bodies. +// message, with a body streamed from it as the app reads it, and writes +// the handler's Response the way the package's listener does (header +// record, content length for bodies that complete within the first reads, +// streamed bodies, error responses), so a client sees the same status +// lines, headers and bodies. A body the app leaves unread is drained after +// the response, within the package's limits. // // HTTP/1.1 only: the http2/https server options and WebSocket upgrades are // not declared. A Response that is still exactly its constructor arguments @@ -136,27 +138,125 @@ function requestHeaders(incoming: IncomingMessage): Headers { return headers; } -function readIncoming(incoming: IncomingMessage): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - incoming.on("data", (chunk: Buffer) => { - chunks.push(chunk); - }); - incoming.on("end", () => resolve(chunks.length === 1 ? chunks[0]! : Buffer.concat(chunks))); - incoming.on("error", reject); +/** What the listener knows about the incoming body: whether it ended + * (the package's `readableEnded`) and whether a drain started. */ +type IncomingState = { ended: boolean; draining: boolean }; + +/** The request body as the package streams it: each pull reads one chunk + * of the incoming message, which pauses between pulls, so the socket + * holds what the app has not read yet. The stream pulls once on its own + * when it is constructed; the package builds its stream only when the app + * reads the body, so until the app's first read a drain discards the + * chunks instead of pausing for them. */ +function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream { + let pulls = 0; + let listening = false; + let settled = false; + let wake: (() => void) | undefined = undefined; + const woken = (): void => { + const resolve = wake; + wake = undefined; + if (resolve !== undefined) resolve(); + }; + return new ReadableStream({ + pull(controller) { + pulls++; + return new Promise((resolve) => { + if (settled) { + resolve(); + return; + } + wake = resolve; + if (listening) { + incoming.resume(); + return; + } + listening = true; + incoming.on("data", (chunk: Buffer) => { + if (state.draining && pulls === 1) return; + incoming.pause(); + if (settled) return; + controller.enqueue(chunk); + woken(); + }); + incoming.on("end", () => { + if (settled) return; + settled = true; + controller.close(); + woken(); + }); + incoming.on("error", (err: Error) => { + if (settled) return; + settled = true; + controller.error(err); + woken(); + }); + }); + }, }); } -async function newRequest( +function newRequest( incoming: IncomingMessage, defaultHostname: string | undefined, -): Promise { + state: IncomingState, +): Request { const method = normalizeIncomingMethod(incoming.method); const url = requestUrl(incoming, defaultHostname); const headers = requestHeaders(incoming); if (method === "GET" || method === "HEAD") return new Request(url, { method, headers }); - const body = await readIncoming(incoming); - return new Request(url, { method, headers, body }); + return new Request(url, { + method, + headers, + body: incomingBody(incoming, state), + duplex: "half", + }); +} + +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; + +/** The package's drain of a body the app left unread: read and discard + * up to MAX_DRAIN_BYTES or DRAIN_TIMEOUT_MS, then close the connection + * once its writes are out. */ +function drainIncoming(incoming: IncomingMessage, state: IncomingState): void { + const socket = incoming.socket; + if (socket.destroyed || state.draining) return; + state.draining = true; + let bytesRead = 0; + let done = false; + const cleanup = (): void => { + if (done) return; + done = true; + clearTimeout(timer); + }; + const forceClose = (): void => { + if (done) return; + cleanup(); + if (!socket.destroyed) socket.destroySoon(); + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref(); + incoming.on("data", (chunk: Buffer) => { + if (done) return; + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +} + +/** The package's close handler cleanup: an unread body drains two timer + * turns after the response closes. */ +function drainAfterClose(incoming: IncomingMessage, state: IncomingState): void { + if (state.ended) return; + setTimeout(() => { + if (!state.ended) + setTimeout(() => { + drainIncoming(incoming, state); + }); + }); } /** The package's outgoing header record: Headers order (lowercase, @@ -427,6 +527,14 @@ async function respond( await writeResponse(res, outgoing, head); } +/** A Response the package writes from its cache without waiting: still + * exactly its constructor arguments, with no body or a string or bytes + * body. */ +function immediateCacheable(res: Response): boolean { + const light = scriptcResponseLight(res); + return light[0] !== "0" && Number(light[2]!) >= -1; +} + function handleFetchError(e: unknown): Response { return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500); } @@ -439,13 +547,38 @@ async function handleRequest( ): Promise { let req: Request | undefined = undefined; let res: Response | Promise | undefined = undefined; + const state: IncomingState = { ended: false, draining: false }; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = (): void => { + if (req === undefined || closeHandlerAttached) return; + closeHandlerAttached = true; + if (needsBodyCleanup) + outgoing.on("close", () => { + drainAfterClose(incoming, state); + }); + }; try { - req = await newRequest(incoming, options.hostname); + req = newRequest(incoming, options.hostname, state); + needsBodyCleanup = + options.autoCleanupIncoming !== false && + !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) + incoming.on("end", () => { + state.ended = true; + }); res = fetchCallback(req, { incoming, outgoing }); + if (!(res instanceof Promise) && immediateCacheable(res)) { + if (needsBodyCleanup) + outgoing.once("finish", () => { + if (!state.ended) drainIncoming(incoming, state); + }); + } else ensureCloseHandler(); } catch (e) { if (res !== undefined) return handleResponseError(e, outgoing); const errorHandler = options.errorHandler; if (errorHandler !== undefined) { + ensureCloseHandler(); const handled = await errorHandler( req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e), ); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8c7d44cdb..937f9ff6b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1319,6 +1319,7 @@ export const LIB_FN_SYMS: Record = { "http.reqPipeClient": "scr_http_req_pipe_client", "http.reqPipeSock": "scr_http_req_pipe_sock", "http.reqResume": "scr_http_req_resume", + "http.reqPause": "scr_http_req_pause", "http.reqDestroy": "scr_http_req_destroy", "http.reqSetEncoding": "scr_http_req_set_encoding", "http.resSetHeader": "scr_http_res_set_header", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index d9746e1f6..cb79accea 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -7581,7 +7581,7 @@ function lowerHttpReqMethodCall( loc, }; } - if (name === "resume" || name === "destroy") { + if (name === "resume" || name === "pause" || name === "destroy") { requireStatementPosition(lowerer, call, `req.${name}()`); if (args.length !== 0) { lowerer.noLowering( @@ -7591,7 +7591,8 @@ function lowerHttpReqMethodCall( ); } const receiver = coerceToHandle(lowerer, access.expression, HTTPREQ_T); - const fn: IrLibFn = name === "resume" ? "http.reqResume" : "http.reqDestroy"; + const fn: IrLibFn = + name === "resume" ? "http.reqResume" : name === "pause" ? "http.reqPause" : "http.reqDestroy"; return { kind: "libCall", fn, args: [receiver], type: VOID, loc }; } if (name === "setEncoding") { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8d2de2a87..2b12c15ab 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1835,7 +1835,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * connection (+1, the same handle net.connect would give); * sockRemoteAddress answers `string | undefined` (NULL after the fd * closed, Node's destroyed-socket undefined; a dual-stack accept of an - * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqDestroy and + * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and * the req error/close listener slots complete the IncomingMessage * surface portless's client responses use; resDestroy/resOnClose and * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat @@ -1902,6 +1902,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, "http.reqOnClose": { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json new file mode 100644 index 000000000..3a4358d78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7287-http-held-request-backpressure.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index cb88297bf..29eb52ef7 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -239,10 +239,11 @@ struct ScrHttpReq { bool close_queued; bool close_emitted; /* settled: err/close listeners dropped */ bool join_dup; /* joinDuplicateHeaders: repeated names read joined ", " */ - /* pause(): delivery holds — arrived body bytes buffer in pend (the - * parser keeps consuming; memory is the buffer, a documented bound) - * and 'end' defers (end_pending) until resume() drains through the - * emit queue, never the resuming stack. */ + /* pause(): delivery holds — arrived body bytes buffer in pend and + * 'end' defers (end_pending) until resume() drains through the emit + * queue, never the resuming stack. A held HTTP/1 server request stops + * its socket's reads once pend reaches SCR_HTTP_REQ_HOLD_BYTES + * (sock_held), as Node's readStop does. */ bool paused; bool end_pending; bool drain_queued; @@ -257,6 +258,7 @@ struct ScrHttpReq { bool received; /* the whole body arrived (req.complete) */ bool body_seen; /* body bytes arrived */ bool destroyed; /* req.destroy()/the teardown ran — req.destroyed */ + bool sock_held; /* this request stopped its socket's reads */ ScrNetLs aborted_ls; /* req.on('aborted') — the h2 compat event */ }; @@ -330,7 +332,10 @@ void scr_http_req_release(ScrHttpReq *r) { scr_http_client_release(r->pipe_client); scr_net_sock_release(r->pipe_sock); free(r->pend); - if (r->sock) scr_net_sock_release(r->sock); + if (r->sock) { + if (r->sock_held) scr_net_sock_hold(r->sock, false); + scr_net_sock_release(r->sock); + } if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream); #ifdef SCR_RC_AUDIT scr_http_live--; @@ -638,6 +643,22 @@ static bool scr_http_req_holding(const ScrHttpReq *r) { return r->paused || (r->gated && !r->flowing); } +/* IncomingMessage's readableHighWaterMark (its socket's, 64 KiB): a held + * request buffers this many body bytes before its socket stops reading. */ +#define SCR_HTTP_REQ_HOLD_BYTES 65536u + +/* Stops the socket's reads while an HTTP/1 server request holds a full + * buffer, and restarts them once it flows, ends or is dumped, as Node's + * readStop when push() returns false. Client responses and h2 compat + * requests keep reading. */ +static void scr_http_req_update_hold(ScrHttpReq *r) { + bool hold = r->sock != NULL && r->status == -1 && !r->http2 && !r->ended && + scr_http_req_holding(r) && r->pend_len >= SCR_HTTP_REQ_HOLD_BYTES; + if (hold == r->sock_held) return; + r->sock_held = hold; + scr_net_sock_hold(r->sock, hold); +} + static void scr_http_req_queue_drain(ScrHttpReq *r) { if (r->drain_queued) return; r->drain_queued = true; @@ -650,6 +671,7 @@ static void scr_http_req_flow(ScrHttpReq *r) { r->paused = false; r->flowing = true; if (r->pend_len > 0 || r->end_pending) scr_http_req_queue_drain(r); + scr_http_req_update_hold(r); } /* A 'data' listener or pipe() starts the flow unless pause() stopped it, @@ -658,9 +680,7 @@ static void scr_http_req_consume(ScrHttpReq *r) { if (r->gated && !r->paused && !r->flowing) scr_http_req_flow(r); } -/* resume(): releases what pause() or the initial paused state held. This - * parser always consumes body bytes from the socket (SEMANTICS.md); held - * bytes buffer in memory. */ +/* resume(): releases what pause() or the initial paused state held. */ void scr_http_req_resume(ScrHttpReq *r) { if (!scr_http_req_holding(r)) return; scr_http_req_flow(r); @@ -676,6 +696,7 @@ static void scr_http_req_dump(ScrHttpReq *r) { r->pend_len = r->pend_cap = 0; r->paused = false; r->flowing = true; + scr_http_req_update_hold(r); } /* pause(): delivery holds until resume() (scr_http_req_deliver buffers; @@ -759,6 +780,7 @@ static void scr_http_req_finish(ScrHttpReq *r, bool fire) { return; } r->ended = true; + scr_http_req_update_hold(r); r->end_consumed = fire && (r->data_ls.n > 0 || r->pipe_res || r->pipe_client || r->pipe_sock); if (fire) scr_net_fire0_this(&r->end_ls, r, SCR_DYNH_HTTP_REQ); scr_net_ls_drop(&r->data_ls); @@ -2305,6 +2327,7 @@ static void scr_http_proto_sweep(void) { req->pend_len = req->pend_cap = 0; if (held_len > 0) scr_http_req_deliver(req, held, held_len); free(held); + scr_http_req_update_hold(req); if (!scr_exc_pending() && !req->paused && req->end_pending) { req->end_pending = false; scr_http_req_finish(req, true); @@ -2758,8 +2781,8 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) { if (!r || n == 0 || r->ended) return; r->body_seen = true; if (scr_http_req_holding(r)) { - /* paused: the parser keeps consuming (memory is the buffer — a - * documented bound), delivery waits for the drain */ + /* held: delivery waits for the drain; a full buffer stops the + * socket's reads */ if (r->pend_len + n > r->pend_cap) { size_t cap = r->pend_cap ? r->pend_cap : 4096; while (cap < r->pend_len + n) cap *= 2; @@ -2769,6 +2792,7 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) { } memcpy(r->pend + r->pend_len, data, n); r->pend_len += n; + scr_http_req_update_hold(r); return; } bool piped = r->pipe_res != NULL || r->pipe_client != NULL || r->pipe_sock != NULL; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 29a4af393..4d5a37f77 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -556,6 +556,10 @@ struct ScrNetSocket { * unconsumed stream). resume() clears user_paused and sets flowing. */ bool user_paused; bool flowing; + /* scr_net_sock_hold: a protocol consumer (an HTTP/1 server request + * holding unread body bytes) stops reads without touching the user's + * pause state */ + bool hold; /* EOF arrived while the socket buffered for a consumer it does not have * yet: 'end' waits until one drains the buffer (Node's readable state) */ bool eof_pending; @@ -978,7 +982,7 @@ static ScrNetSocket *scr_net_sock_new(void) { #define SCR_NET_READ_AHEAD 65536u static void scr_net_sock_update_read(ScrNetSocket *s) { - bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && + bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && !s->hold && (s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL || (s->tunnel != NULL && !s->tunnel->send_only) /* the proxy's CONNECT answer */ || s->flowing /* resume(): flow (and discard sans listeners) */ || @@ -1303,7 +1307,7 @@ static void scr_net_sock_read(ScrNetSocket *s) { if (s->tunnel != NULL) return; /* readiness feeds the tunnel pump instead */ if (s->tops && !s->t_est) return; /* readiness feeds the handshake pump instead */ for (;;) { - if (s->fd < 0 || s->user_paused) return; + if (s->fd < 0 || s->user_paused || s->hold) return; bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing; /* buffered (peeked/unshifted) bytes first — they precede the * kernel's in the stream; a TLS engine drains them through its bio */ @@ -3102,6 +3106,15 @@ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s) { return scr_net_sock_retain(s); } +/* Stops (hold) or restarts reads for a protocol consumer that has + * buffered as much as it accepts, as Node's readStop when push() returns + * false. Separate from pause()/resume(), which stay the user's. */ +void scr_net_sock_hold(ScrNetSocket *s, bool hold) { + if (s->hold == hold) return; + s->hold = hold; + scr_net_sock_update_read(s); +} + /* socket.resume(): flowing mode — a consumer even with no 'data' * listener (arrived bytes discard, so 'end' can be reached, Node's * resumed-but-unconsumed stream). Buffered bytes deliver from the next @@ -5271,7 +5284,7 @@ static bool scr_net_flags_pending(void) { * plaintext inside a transport, or unshifted bytes in the receive * buffer: the poller can't re-signal them, the sweep must deliver */ bool consumers = (s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing) && - !s->user_paused; + !s->user_paused && !s->hold; if (consumers && s->fd >= 0) { if (s->tops && s->t_est && s->tops->pending && s->tops->pending(s->tctx)) return true; if (!s->tops && s->rlen > s->rhead) return true; @@ -5377,7 +5390,7 @@ static void scr_net_sweep(void) { while (sock) { ScrNetSocket *next = sock->next; /* may unregister below */ scr_net_sock_retain(sock); /* callbacks may drop every other ref */ - if (sock->fd >= 0 && !sock->user_paused && + if (sock->fd >= 0 && !sock->user_paused && !sock->hold && (sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data || sock->flowing) && ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) || (!sock->tops && sock->rlen > sock->rhead))) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index ffa9b2f72..6df6945ea 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -232,6 +232,7 @@ void scr_net_sock_destroy(ScrNetSocket *s); * counters/flags the compat surface reads. */ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s); /* +1: chaining */ ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s); /* +1: chaining */ +void scr_net_sock_hold(ScrNetSocket *s, bool hold); /* protocol read hold (not pause) */ ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable); /* +1: chaining */ void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms); void scr_net_sock_destroy_soon(ScrNetSocket *s); diff --git a/tests/corpus/7287-http-held-request-backpressure.ts b/tests/corpus/7287-http-held-request-backpressure.ts new file mode 100644 index 000000000..1953b8ec6 --- /dev/null +++ b/tests/corpus/7287-http-held-request-backpressure.ts @@ -0,0 +1,106 @@ +// A server request that holds its body (nothing reads it yet, or its +// reader paused it) stops reading its socket once its buffer is full, so +// a client that keeps uploading is held back by TCP. Each client writes +// 64 KiB chunks, one after the previous write's callback, and stops when +// the response arrives. An unread body followed by a pipelined request is +// discarded once the response finishes, and the second request is served. +import * as http from "node:http"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; + +const MiB = 1024 * 1024; + +function upload(port: number, path: string): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const frame = Buffer.concat([Buffer.from("10000\r\n"), Buffer.alloc(65536, 98), Buffer.from("\r\n")]); + let sent = 0; + let sentBeforeResponse = -1; + let response = ""; + const writeNext = (): void => { + if (sentBeforeResponse >= 0 || socket.destroyed || sent >= 256 * MiB) return; + sent += 65536; + socket.write(frame, () => { + writeNext(); + }); + }; + socket.on("connect", () => { + socket.write(`POST ${path} HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n`); + writeNext(); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + if (sentBeforeResponse < 0 && response.includes("\r\n\r\n")) { + sentBeforeResponse = sent; + socket.destroy(); + resolve(sentBeforeResponse); + } + }); + socket.on("error", () => {}); + }); +} + +function pipelined(port: number): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const body = "c".repeat(300000); + let response = ""; + socket.on("connect", () => { + socket.write( + `POST /unread HTTP/1.1\r\nHost: localhost\r\nContent-Length: ${body.length}\r\n\r\n${body}` + + "GET /second HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + }); + socket.on("end", () => { + const lines = response.split("\r\n").filter((line) => line.startsWith("HTTP/") || line.startsWith("served")); + resolve(lines); + }); + }); +} + +let pausedRequest: http.IncomingMessage | undefined; + +const server = http.createServer((req, res) => { + if (req.url === "/unconsumed") { + setTimeout(() => { + res.writeHead(413, { "content-type": "text/plain" }); + res.end("unconsumed"); + }, 300); + return; + } + if (req.url === "/paused") { + let seen = 0; + req.on("data", (chunk: Buffer) => { + seen += chunk.length; + if (pausedRequest === undefined) { + pausedRequest = req; + req.pause(); + setTimeout(() => { + res.writeHead(413, { "content-type": "text/plain" }); + res.end(`paused after ${seen > 0}`); + req.resume(); + }, 300); + } + }); + return; + } + res.writeHead(200, { "content-type": "text/plain" }); + res.end(`served ${req.url}`); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + void (async () => { + const before = process.memoryUsage().rss; + const unconsumed = await upload(port, "/unconsumed"); + console.log("unconsumed: client stopped before 32 MiB:", unconsumed < 32 * MiB); + const paused = await upload(port, "/paused"); + console.log("paused: client stopped before 32 MiB:", paused < 32 * MiB); + console.log("memory grew under 64 MiB:", process.memoryUsage().rss - before < 64 * MiB); + for (const line of await pipelined(port)) console.log(line); + server.close(() => console.log("closed")); + })(); +}); diff --git a/tests/fixtures/npm/cases/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts index b47dae8e3..3cca4aa44 100644 --- a/tests/fixtures/npm/cases/hono-node-server/main.ts +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -2,9 +2,11 @@ // serving a fetch handler: a raw HTTP client prints each exchange's status // line, wire-order headers (Date dropped) and body, covering buffered and // streamed bodies, header forms, cookies, request bodies, URL handling, -// HEAD, empty statuses, handler errors, and a client that disconnects -// during a streamed body. +// HEAD, empty statuses, handler errors, a client that disconnects during +// a streamed body, and request bodies the handler reads, leaves unread or +// rejects while the upload is still arriving. import * as http from "node:http"; +import * as net from "node:net"; import type { AddressInfo } from "node:net"; import { getRequestListener, RequestError, serve } from "@hono/node-server"; @@ -282,7 +284,104 @@ function startSecond(): void { void (async () => { console.log(await exchange(port, { method: "GET", path: "/ok" })); console.log(await exchange(port, { method: "GET", path: "/fail" })); - second.close(() => console.log("second closed")); + second.close(() => { + console.log("second closed"); + startUploads(); + }); })(); }); } + +// Request bodies stream to the handler. /size reads the whole body. The +// other routes leave it unread: /unread answers at once (the package's +// cached-response path) and /unread-async through a promise, and /reject +// answers 413 after 300 ms while the client keeps uploading, so the server +// must stop reading the socket. +const MiB = 1024 * 1024; + +interface UploadResult { + status: string; + body: string; + sentBeforeResponse: number; +} + +/** A chunked POST over a raw socket: 64 KiB chunks, each written after + * the previous write's callback, until `limit` bytes are out (then the + * last chunk, when `finish` is set) or the response arrives. */ +function chunkedUpload(port: number, path: string, limit: number, finish: boolean): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const frame = Buffer.concat([Buffer.from("10000\r\n"), Buffer.alloc(65536, 97), Buffer.from("\r\n")]); + let sent = 0; + let sentBeforeResponse = -1; + let response = ""; + let settled = false; + const settle = (): void => { + if (settled) return; + settled = true; + const headEnd = response.indexOf("\r\n\r\n"); + const status = response.slice(0, response.indexOf("\r\n")); + resolve({ status, body: headEnd < 0 ? "" : response.slice(headEnd + 4), sentBeforeResponse }); + socket.destroy(); + }; + const writeNext = (): void => { + if (sentBeforeResponse >= 0 || socket.destroyed) return; + if (sent >= limit) { + if (finish) socket.write("0\r\n\r\n"); + return; + } + sent += 65536; + socket.write(frame, () => { + writeNext(); + }); + }; + socket.on("connect", () => { + socket.write(`POST ${path} HTTP/1.1\r\nHost: 127.0.0.1\r\nTransfer-Encoding: chunked\r\n\r\n`); + writeNext(); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + if (sentBeforeResponse < 0 && response.includes("\r\n\r\n")) sentBeforeResponse = sent; + const match = /content-length: (\d+)/i.exec(response); + const headEnd = response.indexOf("\r\n\r\n"); + if (match !== null && headEnd >= 0 && response.length - headEnd - 4 >= Number(match[1])) settle(); + }); + socket.on("error", () => settle()); + socket.on("close", () => settle()); + }); +} + +function startUploads(): void { + const server = serve( + { + fetch: (request: Request): Response | Promise => { + const url = new URL(request.url); + if (url.pathname === "/size") + return request.arrayBuffer().then((body) => new Response(`size ${body.byteLength}`)); + if (url.pathname === "/reject") + return delay(300).then(() => new Response("too large", { status: 413 })); + if (url.pathname === "/unread-async") return Promise.resolve(new Response("unread async")); + return new Response(`unread ${url.pathname}`); + }, + port: 0, + hostname: "127.0.0.1", + }, + (info: AddressInfo) => { + void (async () => { + const read = await chunkedUpload(info.port, "/size", 3 * MiB, true); + console.log(`### chunked POST /size\n${read.status}\n${read.body}\n`); + console.log(await exchange(info.port, { method: "POST", path: "/unread", body: "x".repeat(300000) })); + console.log(await exchange(info.port, { method: "GET", path: "/after-unread" })); + console.log(await exchange(info.port, { method: "POST", path: "/unread-async", body: "y".repeat(300000) })); + console.log(await exchange(info.port, { method: "GET", path: "/after-unread-async" })); + const before = process.memoryUsage().rss; + const rejected = await chunkedUpload(info.port, "/reject", 512 * MiB, false); + const grown = process.memoryUsage().rss - before; + console.log(`### chunked POST /reject\n${rejected.status}\n${rejected.body}`); + console.log("client stopped before 32 MiB:", rejected.sentBeforeResponse >= 0 && rejected.sentBeforeResponse < 32 * MiB); + console.log("server memory grew under 64 MiB:", grown < 64 * MiB); + server.close(() => console.log("uploads closed")); + })(); + }, + ); +} diff --git a/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json b/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json new file mode 100644 index 000000000..9622e52d1 --- /dev/null +++ b/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json @@ -0,0 +1 @@ +{ "plain": 5600, "san": 5600 } From ed1786869b4fb76d8a9f21de32b4cf57d4bf6886 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:06:48 -0500 Subject: [PATCH 161/169] Record a class's bag initializer only when its class-table branches agree A per-class field-creation helper creates an instance's property bag with the class's recorded initializer. A class can carry several: three.js's Material bag is created with Material's own prototype for Material instances and through the per-instance prototype dispatch for subclass instances, and the first one was recorded. MeshStandardMaterial instances then got bags with Material's prototype, and writes to their own fields (roughness, metalness, color) through setValues stored into the bag. The initializer is now recorded only when every branch's initializer is the same; other classes create their bags through the class tables. --- .../lowering/class-dynamic-dispatch.test.ts | 50 +++++++++++++++++++ .../lowering/class-dynamic-dispatch.ts | 17 +++++++ 2 files changed, 67 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index 1a21a6aa5..bbdf271c0 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -560,3 +560,53 @@ test("class branches read their exact class's bag, also for classes with subclas then: [{ kind: "return", value: { kind: "dynObjLit", fields: [] } }], }); }); + +test("records a class's bag initializer only when all its class-table branches agree", () => { + const { lowerer } = context(); + const classBagInitializers = new Map(); + Object.assign(lowerer, { classBagInitializers }); + for (const name of ["Material", "Texture"]) + lowerer.classes.set(name, { def: { name, tracksOwnFields: true } } as unknown as ClassInfo); + const initializer = ( + className: string, + prototype: string, + start: number, + ): Extract => { + const at = { ...loc, start }; + return { + kind: "fieldSet", + obj: { + kind: "dynCheck", + value: { kind: "varRef", localId: "p.0", type: DYN, loc: at }, + type: { kind: "object", className }, + loc: at, + }, + className, + field: "%bag", + value: { + kind: "libCall", + fn: "dyn.objCreate", + args: [{ kind: "call", callee: prototype, args: [], type: DYN, loc: at }], + type: DYN, + loc: at, + }, + loc: at, + }; + }; + const dispatch = new ClassDynamicDispatch(); + const tables = (dispatch as unknown as { bagInitializers: Map }) + .bagInitializers; + // A base class whose own instances get its prototype and whose subclass + // instances get theirs through a per-instance dispatch has no single bag. + tables.set("Material", [ + initializer("Material", "%prototype.data.Material", 1), + initializer("Material", "%class.bagPrototype:Material", 2), + ]); + // Branches that differ only in source location agree. + tables.set("Texture", [ + initializer("Texture", "%prototype.data.Texture", 3), + initializer("Texture", "%prototype.data.Texture", 4), + ]); + dispatch.finalize(lowerer); + expect([...classBagInitializers.keys()]).toEqual(["Texture"]); +}); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a1bc41782..ab3ebb34e 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -982,6 +982,11 @@ function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: S ]; } +/** `value` as JSON without its source locations. */ +function withoutLocations(value: IrExpr): string { + return JSON.stringify(value, (key, field: unknown) => (key === "loc" ? undefined : field)); +} + /** Whether `value` reads local `id`. */ function mentionsLocal(value: IrExpr, id: string): boolean { return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`); @@ -4903,6 +4908,18 @@ export class ClassDynamicDispatch { const self = varRef("this", { kind: "object", className }, init.loc); const value = withTypedReceiver(structuredClone(init.value), self); if (mentionsLocal(value, "p.0")) continue; + // The class-table branches can give the class's instances different + // bags (a base class's own prototype for its instances, the + // instance's class prototype for subclass instances); only a class + // whose branches all agree has one initializer for every instance. + const shape = withoutLocations(value); + if ( + initializers.some( + (other) => + withoutLocations(withTypedReceiver(structuredClone(other.value), self)) !== shape, + ) + ) + continue; // Lowerer stand-ins in unit tests may not carry the table. lowerer.classBagInitializers?.set(className, value); } From eb2fa3c829fff0433b890cf92bc41690d63bde6e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:14:51 -0500 Subject: [PATCH 162/169] Fire spawn listeners for a child that exits before its first poll pass A child spawned from a listener that runs during a poll pass, such as a 'close' listener, can exit before the next pass fires its 'spawn' listeners. The same pass's waitpid loop then settled it with 'spawn' never emitted. Settling an exited child now fires its pending 'spawn' listeners first, before its stream data and 'exit', as Node emits them. --- .../7640-child-spawn-before-fast-exit.ts.json | 6 ++++++ packages/runtime/src/scr_child.c | 10 +++++++--- .../7640-child-spawn-before-fast-exit.ts | 20 +++++++++++++++++++ .../7640-child-spawn-before-fast-exit.ts.json | 1 + 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json create mode 100644 tests/corpus/7640-child-spawn-before-fast-exit.ts create mode 100644 tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json new file mode 100644 index 000000000..0b82f6091 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7640-child-spawn-before-fast-exit.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index df7543500..a70b10894 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2780,7 +2780,8 @@ static void scr_child_settle(ScrChild *c) { scr_children_reffed_n--; } if (c->state == SCR_CHILD_EXITED) { - scr_child_stream_drain_now(c->out_stream); + if (!c->spawn_done) scr_child_fire_spawn(c); + if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream); if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream); if (!scr_exc_pending()) scr_ipc_settle(c->ipc); } @@ -5607,9 +5608,12 @@ static void scr_child_settle(ScrChild *c) { /* Streams first, settled after: an exit listener registered from a * draining 'data'/'end' callback still fires below, like Node's. The * SPAWN-FAILURE path skips the drain — Node fires 'error' first and - * the streams' immediate EOF delivers 'end' on the next turn. */ + * the streams' immediate EOF delivers 'end' on the next turn. A child + * spawned during this pass can exit before the next pass fires 'spawn'; + * Node emits 'spawn' before any data or 'exit', so it fires here first. */ if (c->state == SCR_CHILD_EXITED) { - scr_child_stream_drain_now(c->out_stream); + if (!c->spawn_done) scr_child_fire_spawn(c); + if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream); if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream); if (!scr_exc_pending()) scr_ipc_settle(c->ipc); } diff --git a/tests/corpus/7640-child-spawn-before-fast-exit.ts b/tests/corpus/7640-child-spawn-before-fast-exit.ts new file mode 100644 index 000000000..9c232d96c --- /dev/null +++ b/tests/corpus/7640-child-spawn-before-fast-exit.ts @@ -0,0 +1,20 @@ +// A child spawned from a 'close' listener that exits before the listener +// returns still emits 'spawn' before 'exit'. +import { spawn } from "node:child_process"; + +function busyWait(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +const events: string[] = []; +spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }).on("close", () => { + const child = spawn("sh", ["-c", "exit 4"], { stdio: "ignore" }); + child.on("spawn", () => events.push("spawn")); + child.on("exit", (code) => events.push(`exit ${code}`)); + child.on("close", (code) => { + events.push(`close ${code}`); + console.log(events.join(", ")); + }); + busyWait(300); +}); diff --git a/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json b/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } From 47490f0bca3cd9657ae6d7fb1f3b6b7f76a13c56 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:16:33 -0500 Subject: [PATCH 163/169] Revert "Reuse the bundled declaration files' diagnostics across builds (#1981)" --- .../src/frontend/ts7/lib-diagnostics.test.ts | 221 ------------ .../src/frontend/ts7/lib-diagnostics.ts | 324 ------------------ .../compiler/src/frontend/ts7/program-host.ts | 35 +- packages/compiler/src/frontend/ts7/session.ts | 30 -- packages/compiler/src/index.ts | 21 -- packages/compiler/src/native/cli.ts | 17 +- 6 files changed, 3 insertions(+), 645 deletions(-) delete mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts delete mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.ts diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts deleted file mode 100644 index f041a6cb5..000000000 --- a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, afterEach, expect, test } from "vitest"; -import type { SourceFile } from "./ast-types.js"; -import { - reusedLibDiagnostics, - setLibDiagnosticsStore, - type LibDiagnosticsStore, -} from "./lib-diagnostics.js"; -import type { Ts7DiagnosticData } from "./session-schema.generated.js"; -import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js"; -import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; - -const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); -const lib = join(dir, "lib.es5.d.ts"); -const fallback = join(dir, "scriptc-node-fallback.d.ts"); -const libText = "declare var NaN: number;\ninterface Array { length: number }\n"; -const fallbackText = "export {};\ndeclare global {\n var process: { argv: string[] };\n}\n"; -writeFileSync(lib, libText); -writeFileSync(fallback, fallbackText); -const main = join(dir, "main.ts"); -const util = join(dir, "\u{1F600}.ts"); -const other = join(dir, "�.ts"); - -afterEach(() => setLibDiagnosticsStore(null)); -afterAll(() => { - closeSourceParser(); - rmSync(dir, { recursive: true, force: true }); -}); - -function memoryStore(): LibDiagnosticsStore & { entries: Map } { - const entries = new Map(); - return { - entries, - read: (key) => entries.get(key) ?? null, - write: (key, text) => void entries.set(key, text), - }; -} - -function diagnostic(fileName: string | undefined, pos: number, code = 2322): Ts7DiagnosticData { - return { fileName, pos, end: pos + 1, code, category: 1, text: `at ${pos}` }; -} - -/** A session program whose full-check and checked-file answers are given; it - * records which requests it was asked. */ -function sessionProgram(answers: { - semantic: Map; - global?: Ts7DiagnosticData[]; - checked?: boolean; -}): Ts7SessionProgram & { asked: string[] } { - const asked: string[] = []; - const all = [...answers.semantic.values()].flat(); - return { - asked, - getCompilerOptions: () => ({ strict: true }), - diagnosticData: (method: string, file?: string) => { - asked.push(file === undefined ? method : `${method} ${file}`); - if (method === "getGlobalDiagnostics") return answers.global ?? []; - return file === undefined ? all : (answers.semantic.get(file) ?? []); - }, - checkedFileDiagnosticData: (files: readonly string[]): Ts7CheckedFileDiagnosticData | null => { - asked.push(`checked ${files.join(",")}`); - if (answers.checked === false) return null; - return { files: files.map((file) => answers.semantic.get(file) ?? []), global: [] }; - }, - } as unknown as Ts7SessionProgram & { asked: string[] }; -} - -/** The program's files, parsed: the supplied two and the given others. */ -function files(texts: Record): (name: string) => SourceFile | undefined { - const parsed = new Map([ - [lib, parseSourceFile(lib, libText, "ts")], - [fallback, parseSourceFile(fallback, fallbackText, "ts")], - ]); - for (const [name, text] of Object.entries(texts)) - parsed.set(name, parseSourceFile(name, text, "ts")); - return (name) => parsed.get(name); -} - -const names = [lib, fallback, main]; -const supplied = (name: string): boolean => name === lib || name === fallback; -const moduleFile = files({ [main]: "export const n: number = 'x';\n" }); - -test("without a store the full check answers", () => { - const program = sessionProgram({ semantic: new Map() }); - expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toBeNull(); - expect(program.asked).toEqual([]); -}); - -test("a stored answer stands in for checking the supplied files, merged by file name", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([ - [lib, [diagnostic(lib, 1)]], - [fallback, []], - [main, [diagnostic(main, 5), diagnostic(main, 9)]], - ]); - const first = sessionProgram({ semantic }); - const full = reusedLibDiagnostics(first, names, supplied, moduleFile); - // The first build runs the full check and stores the supplied files' answers. - expect(first.asked).toEqual([ - "getGlobalDiagnostics", - "getSemanticDiagnostics", - "checked ", - `getSemanticDiagnostics ${lib}`, - `getSemanticDiagnostics ${fallback}`, - ]); - expect(store.entries.size).toBe(1); - const second = sessionProgram({ semantic }); - const reused = reusedLibDiagnostics(second, names, supplied, moduleFile); - expect(second.asked).toEqual([`checked ${main}`]); - // The files' diagnostics in the order of their names (lib.es5.d.ts, main.ts), as the full answer. - expect(reused).toEqual({ - semantic: [diagnostic(lib, 1), diagnostic(main, 5), diagnostic(main, 9)], - global: [], - }); - expect(full?.semantic).toEqual(reused?.semantic); -}); - -test("files merge in code point order, which differs from UTF-16 order", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const all = [lib, fallback, util, other]; - const sources = files({ [util]: "export {};\n", [other]: "export {};\n" }); - const semantic = new Map([ - [util, [diagnostic(util, 1)]], - [other, [diagnostic(other, 2)]], - ]); - reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); - const reused = reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); - // U+FFFD sorts before U+1F600 by code point (its surrogates sort after it by UTF-16 unit). - expect(reused?.semantic).toEqual([diagnostic(other, 2), diagnostic(util, 1)]); -}); - -test("a full check with global diagnostics is not stored", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const program = sessionProgram({ - semantic: new Map([[main, [diagnostic(main, 1)]]]), - global: [diagnostic(undefined, 0, 2318)], - }); - expect(reusedLibDiagnostics(program, names, supplied, moduleFile)?.global).toEqual([ - diagnostic(undefined, 0, 2318), - ]); - expect(store.entries.size).toBe(0); -}); - -test("scripts reuse the answer unless they declare a global the supplied files declare", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([[main, []]]); - // A script declaring only new names: it stores the answer, and a later one reuses it. - const fresh = files({ - [main]: "const total = 1;\nfunction run() {}\nif (total) { var later = 2; }\n", - }); - reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, fresh); - expect(store.entries.size).toBe(1); - const again = sessionProgram({ semantic }); - expect(reusedLibDiagnostics(again, names, supplied, fresh)).not.toBeNull(); - expect(again.asked).toEqual([`checked ${main}`]); - // Scripts declaring a supplied global, at the top level, in a block, in a - // `declare global` of a supplied module, or an ambient module: the full check. - for (const text of [ - "interface Array { extra: T }\n", - "if (true) { var NaN = 1; }\n", - "declare var process: unknown;\n", - 'declare module "fs" {}\n', - ]) { - const program = sessionProgram({ semantic }); - const result = reusedLibDiagnostics(program, names, supplied, files({ [main]: text })); - expect(program.asked[0], text).not.toBe(`checked ${main}`); - if (result !== null) expect(program.asked[0]).toBe("getGlobalDiagnostics"); - } - expect(store.entries.size).toBe(1); -}); - -test("a program whose script declares a supplied global does not store its answer", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const program = sessionProgram({ semantic: new Map([[main, []]]) }); - reusedLibDiagnostics(program, names, supplied, files({ [main]: "declare var NaN: number;\n" })); - expect(store.entries.size).toBe(0); -}); - -test("modules that can declare something global, and servers without the method, ask as before", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([[main, []]]); - reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); - expect(store.entries.size).toBe(1); - for (const text of [ - "export {};\ndeclare global { var extra: number }\n", - 'export {};\ndeclare module "./other" { const extra: number }\n', - "export as namespace Lib;\nexport const n = 1;\n", - ]) { - const program = sessionProgram({ semantic }); - expect(reusedLibDiagnostics(program, names, supplied, files({ [main]: text }))).toBeNull(); - expect(program.asked).toEqual([]); - } - const old = sessionProgram({ semantic, checked: false }); - expect(reusedLibDiagnostics(old, names, supplied, moduleFile)).toBeNull(); - // Nor does a server without the method store an answer it could not use. - const fresh = memoryStore(); - setLibDiagnosticsStore(fresh, "checker"); - reusedLibDiagnostics(sessionProgram({ semantic, checked: false }), names, supplied, moduleFile); - expect(fresh.entries.size).toBe(0); -}); - -test("a supplied file that changes is a different entry", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([[main, []]]); - reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); - writeFileSync(fallback, fallbackText + "// edited\n"); - const program = sessionProgram({ semantic }); - reusedLibDiagnostics(program, names, supplied, moduleFile); - expect(program.asked[0]).toBe("getGlobalDiagnostics"); - expect(store.entries.size).toBe(2); - writeFileSync(fallback, fallbackText); -}); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts deleted file mode 100644 index 827b337bd..000000000 --- a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts +++ /dev/null @@ -1,324 +0,0 @@ -/* The semantic diagnostics of the declaration files scriptc supplies (the - * TypeScript libs and scriptc's own declaration files), reused across - * builds. Checking them is most of the full check of a small program. When - * no other file of a program can change what checking those files reports - * (every one is a module without global or module augmentations, or a - * script declaring no global name the supplied files declare), the answer - * depends only on the checker, the compiler options and the files - * themselves, so a stored answer for the same three stands in for checking - * them again: the server checks only the program's other files - * (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the - * order the full check sorts them, by file name. An answer is stored only - * when the full check reports no global diagnostics: the global diagnostics - * of checking the other files are then the full check's. */ -import { createHash } from "node:crypto"; -import { statSync } from "node:fs"; -import type { Node, SourceFile } from "./ast-types.js"; -import type { Ts7DiagnosticData } from "./session-schema.generated.js"; -import type { Ts7SessionProgram } from "./session.js"; -import { NodeFlags, SyntaxKind } from "./enums.js"; -import { walkPreorder } from "./ast.js"; - -/** Key-value storage for reused diagnostics (best effort: failures miss). */ -export interface LibDiagnosticsStore { - read(key: string): string | null; - write(key: string, text: string): void; -} - -const SCHEMA = "scriptc.lib-diagnostics.v1"; - -let configured: { store: LibDiagnosticsStore; checker: string } | null = null; - -/** Has later builds of this process reuse the diagnostics of the supplied - * declaration files through `store`, for the checker identified by - * `checker` (its executable's identity), or stop reusing them (null). */ -export function setLibDiagnosticsStore(store: LibDiagnosticsStore | null, checker = ""): void { - configured = store === null ? null : { store, checker }; -} - -/** A checker executable's identity for the stored diagnostics' key: its - * path and file identity, or the path alone when it cannot be read. */ -export function checkerExecutableIdentity(path: string): string { - try { - const info = statSync(path); - return JSON.stringify([path, info.size, info.mtimeMs, info.ino]); - } catch { - return JSON.stringify([path]); - } -} - -/** The program's semantic and global diagnostics as the full check reports - * them, using stored diagnostics for the supplied declaration files; null - * when they cannot be reused for this program (then the caller asks for - * them as before). `supplied` tells whether a program file is one of the - * supplied declaration files. */ -export function reusedLibDiagnostics( - program: Ts7SessionProgram, - fileNames: readonly string[], - supplied: (fileName: string) => boolean, - sourceFile: (fileName: string) => SourceFile | undefined, -): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { - if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; - const { store, checker } = configured; - const libs: string[] = []; - const own: string[] = []; - for (const name of fileNames) (supplied(name) ? libs : own).push(name); - if (libs.length === 0) return null; - // The global names each of the program's scripts declares; a module that - // declares nothing global contributes none. - const scripts: Set[] = []; - for (const name of own) { - const file = sourceFile(name); - if (file === undefined) return null; - if (file.externalModuleIndicator !== undefined) { - if (!declaresNothingGlobal(file)) return null; - continue; - } - const names = isJsFileName(name) ? null : scriptGlobalNames(file); - if (names === null) return null; - scripts.push(names); - } - // A script's globals cannot change what the supplied files report unless - // one of them is also a global the supplied files declare. - const collides = (globals: ReadonlySet): boolean => - scripts.some((names) => [...names].some((name) => globals.has(name))); - // The options without the configuration file's path: it names where the - // project is, which checking the supplied files does not read (the files a - // project's location brings in are part of the key). - const options = { ...program.getCompilerOptions(), configFilePath: undefined }; - const key = entryKey(checker, options, libs); - if (key === null) return null; - const stored = readEntry(store, key, libs); - if (stored !== null && !collides(stored.globals)) { - const checked = program.checkedFileDiagnosticData(own); - if (checked === null) return null; - const byFile = new Map(stored.files); - own.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); - return { semantic: inFileOrder(byFile), global: checked.global }; - } - // The full check, as before. Its answers for the supplied files are stored - // when it reports no global diagnostics and the program's scripts declare - // none of the supplied files' globals (so nothing of the program reached - // what those files report). - const global = program.diagnosticData("getGlobalDiagnostics"); - const semantic = program.diagnosticData("getSemanticDiagnostics"); - // A server that cannot check only some files could never use the answer. - if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) { - const globals = suppliedGlobalNames(libs, sourceFile); - if (globals !== null && !collides(globals)) { - const files: Record = {}; - for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name); - try { - store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files })); - } catch { - // Best effort: a failed write is a later miss. - } - } - } - return { semantic, global }; -} - -/** Whether a file cannot add to or change the global declarations the - * supplied files declare: a module with no `declare global`, no module - * augmentation and no UMD `export as namespace` declaration. */ -function declaresNothingGlobal(file: SourceFile): boolean { - if (file.externalModuleIndicator === undefined) return false; - if (file.moduleAugmentations.length > 0) return false; - for (const statement of file.statements.nodes) { - if (statement.kind === SyntaxKind.NamespaceExportDeclaration) return false; - // `declare global` (a module declaration named by the identifier `global`). - if ( - statement.kind === SyntaxKind.ModuleDeclaration && - statement.name?.kind === SyntaxKind.Identifier && - statement.name.text === "global" - ) - return false; - } - return true; -} - -function isJsFileName(name: string): boolean { - return /\.[cm]?jsx?$/.test(name); -} - -/** The names a script file declares in the global scope: its top-level - * declarations, and the `var` and function declarations of its top-level - * blocks; null when it declares an ambient module (`declare module "x"`), - * which can merge with one the supplied files declare. Over-approximates: - * a name more only makes the stored answer apply less often. */ -function scriptGlobalNames(file: SourceFile): Set | null { - const names = new Set(); - let ambientModule = false; - walkPreorder(file, (node) => { - if (node === file) return undefined; - if (node.kind === SyntaxKind.ModuleDeclaration && node.name?.kind === SyntaxKind.StringLiteral) - ambientModule = true; - const topLevel = node.parent === file; - if (topLevel || node.kind === SyntaxKind.FunctionDeclaration) addDeclaredNames(node, names); - else if ( - node.kind === SyntaxKind.VariableDeclarationList && - (node.flags & NodeFlags.BlockScoped) === 0 - ) - for (const declaration of node.declarations?.nodes ?? []) - addBindingNames(declaration.name, names); - // Functions, classes and namespaces have scopes of their own. - return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; - }); - return ambientModule ? null : names; -} - -/** The global names the supplied files declare: the declarations of their - * scripts (as scriptGlobalNames finds them, with ambient modules as their - * quoted names) and those of the `declare global` blocks of their modules; - * null when one of them is not available. */ -function suppliedGlobalNames( - libs: readonly string[], - sourceFile: (fileName: string) => SourceFile | undefined, -): Set | null { - const names = new Set(); - for (const name of libs) { - const file = sourceFile(name); - if (file === undefined) return null; - if (file.externalModuleIndicator === undefined) { - walkPreorder(file, (node) => { - if (node === file) return undefined; - if ( - node.kind === SyntaxKind.ModuleDeclaration && - node.name?.kind === SyntaxKind.StringLiteral - ) - names.add(JSON.stringify(node.name.text ?? "")); - if (node.parent === file || node.kind === SyntaxKind.FunctionDeclaration) - addDeclaredNames(node, names); - else if ( - node.kind === SyntaxKind.VariableDeclarationList && - (node.flags & NodeFlags.BlockScoped) === 0 - ) - for (const declaration of node.declarations?.nodes ?? []) - addBindingNames(declaration.name, names); - return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; - }); - continue; - } - for (const statement of file.statements.nodes) { - if ( - statement.kind !== SyntaxKind.ModuleDeclaration || - statement.name?.kind !== SyntaxKind.Identifier || - statement.name.text !== "global" - ) - continue; - for (const inner of statement.body?.statements?.nodes ?? []) addDeclaredNames(inner, names); - } - } - return names; -} - -const GLOBAL_SCOPE_BOUNDARIES = new Set([ - SyntaxKind.FunctionDeclaration, - SyntaxKind.FunctionExpression, - SyntaxKind.ArrowFunction, - SyntaxKind.MethodDeclaration, - SyntaxKind.Constructor, - SyntaxKind.GetAccessor, - SyntaxKind.SetAccessor, - SyntaxKind.ClassDeclaration, - SyntaxKind.ClassExpression, - SyntaxKind.ModuleDeclaration, -]); - -/** The names a statement declares in the scope it is in. */ -function addDeclaredNames(node: Node, names: Set): void { - switch (node.kind) { - case SyntaxKind.VariableStatement: - for (const declaration of node.declarationList?.declarations?.nodes ?? []) - addBindingNames(declaration.name, names); - return; - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - if (node.name?.kind === SyntaxKind.Identifier) names.add(node.name.text ?? ""); - return; - } -} - -function addBindingNames(name: Node | undefined, names: Set): void { - if (name === undefined) return; - if (name.kind === SyntaxKind.Identifier) { - names.add(name.text ?? ""); - return; - } - // Object and array binding patterns. - for (const element of name.elements?.nodes ?? []) addBindingNames(element.name, names); -} - -/** The stored diagnostics' key: the checker, the compiler options and the - * supplied files in program order with each one's file identity; null when - * one of them cannot be read. */ -function entryKey(checker: string, options: unknown, libs: readonly string[]): string | null { - const files: unknown[] = []; - for (const name of libs) { - try { - const info = statSync(name); - files.push([name, info.size, info.mtimeMs, info.ino]); - } catch { - return null; - } - } - return createHash("sha256") - .update(JSON.stringify({ schema: SCHEMA, checker, options, files })) - .digest("hex"); -} - -function readEntry( - store: LibDiagnosticsStore, - key: string, - libs: readonly string[], -): { files: Map; globals: Set } | null { - let text: string | null; - try { - text = store.read(key); - } catch { - return null; - } - if (text === null) return null; - let entry: { schema?: unknown; globals?: unknown; files?: Record }; - try { - entry = JSON.parse(text) as typeof entry; - } catch { - return null; - } - if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals)) - return null; - const files = new Map(); - for (const name of libs) { - const list = entry.files[name]; - if (!Array.isArray(list)) return null; - files.set(name, list); - } - return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) }; -} - -/** Each file's diagnostics (already in the checker's order) in the order the - * checker sorts files: by file name, compared by code point. */ -function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { - const names = [...byFile.keys()].sort(compareCodePoints); - const out: Ts7DiagnosticData[] = []; - for (const name of names) for (const d of byFile.get(name)!) out.push(d); - return out; -} - -function compareCodePoints(a: string, b: string): number { - let i = 0; - let j = 0; - while (i < a.length && j < b.length) { - const x = a.codePointAt(i)!; - const y = b.codePointAt(j)!; - if (x !== y) return x < y ? -1 : 1; - i += x > 0xffff ? 2 : 1; - j += y > 0xffff ? 2 : 1; - } - return i < a.length ? 1 : j < b.length ? -1 : 0; -} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index ec143e401..639086e94 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,10 +46,6 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; -import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; -import { reusedLibDiagnostics } from "./lib-diagnostics.js"; -import { ts7Diagnostic } from "./diagnostic.js"; -import type { Ts7DiagnosticData } from "./session-schema.generated.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -397,22 +393,6 @@ export class Ts7Program { return this.project.program.getSourceFileMetadata(resolve(fileName)); } - /** The semantic and global diagnostics with the stored diagnostics of the - * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ - reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { - this.snapshot.ensureActive(); - const program = this.project.program; - const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); - return reusedLibDiagnostics( - program, - this.getSourceFileNames(), - (name) => - declarations.has(tsgoPath(name)) || - program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true, - (name) => this.getSourceFile(name), - ); - } - isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } @@ -443,20 +423,9 @@ export class Ts7Program { * package dropped the aggregate). Order mirrors 5.9.3: config, options/ * program, global, syntactic, semantic. */ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { - const config = program.getConfigFileParsingDiagnostics(); - const options = program.getProgramDiagnostics(); - const reused = program.reusedLibDiagnostics(); - if (reused !== null) - return [ - ...config, - ...options, - ...reused.global.map(ts7Diagnostic), - ...program.getSyntacticDiagnostics(), - ...reused.semantic.map(ts7Diagnostic), - ]; return [ - ...config, - ...options, + ...program.getConfigFileParsingDiagnostics(), + ...program.getProgramDiagnostics(), ...program.getGlobalDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(), diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 082bb79a6..d3f2ae9fe 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -55,13 +55,6 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; - files?: string[]; -} - -/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ -export interface Ts7CheckedFileDiagnosticData { - files: Ts7DiagnosticData[][]; - global: Ts7DiagnosticData[]; } /** Platform-independent session over an already connected transport. The @@ -351,29 +344,6 @@ export class Ts7SessionProgram { result.push(ts7Diagnostic(data)); return result; } - /** The diagnostics of `method` for `file`, or for the whole program, as - * the server sends them. */ - diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { - return this.session.request(method, this.query(file)) ?? []; - } - /** The semantic diagnostics of each of `files` and the global diagnostics, - * after the server checks only those files (getDiagnosticsOfCheckedFiles), - * or null when the server does not answer that method. */ - checkedFileDiagnosticData(files: readonly string[]): Ts7CheckedFileDiagnosticData | null { - try { - return this.session.request("getDiagnosticsOfCheckedFiles", { - ...this.query(), - files: [...files], - }); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('unknown API method "getDiagnosticsOfCheckedFiles"') - ) - return null; - throw error; - } - } getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 586561632..4f7b93cfd 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -9,7 +9,6 @@ import { } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; -import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -1162,28 +1161,9 @@ async function prepareExecutableInputInChild( * `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM * text, to `resultPath`. */ -/** Has the build reuse the diagnostics of the declaration files scriptc - * supplies (lib-diagnostics.ts) through the build cache, when there is one and - * the checker is tsc-ts. */ -async function configureLibDiagnostics(): Promise { - let checker: string | undefined; - try { - checker = checkerCacheIdentity(); - } catch { - // A missing checker fails where the build starts it. - checker = undefined; - } - const root = checker === undefined ? null : await prepareBuildCacheRoot(buildCacheRoot()); - setLibDiagnosticsStore( - root === null ? null : fileVerdictStore(join(root, "lib-diagnostics-v1")), - checker ?? "", - ); -} - export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; clearCompileSessionCaches(); - await configureLibDiagnostics(); setProvenanceSources(request.provenance); const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; @@ -1246,7 +1226,6 @@ async function compileTracked( const timing = compilationTiming(); const timingStart = Date.now(); entryPath = resolve(entryPath); - await configureLibDiagnostics(); const outputKind = opts.outputKind ?? "exe"; if ( (opts.backend !== undefined && opts.backend !== "llvm") || diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 44d5eb9f0..21047b929 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -16,11 +16,7 @@ import { createNativeTs7Api } from "../frontend/ts7/native-api.js"; import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js"; import { setCompilerReleaseVersion } from "../library/sidecar.js"; import { NativeCompiler } from "./compiler.js"; -import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js"; -import { - checkerExecutableIdentity, - setLibDiagnosticsStore, -} from "../frontend/ts7/lib-diagnostics.js"; +import { openNativeCache } from "./cache.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; @@ -57,17 +53,6 @@ async function main(): Promise { if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot); setCompilerReleaseVersion(toolchain.compilerVersion); cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden)); - if (cache !== null) { - const store = cache; - setLibDiagnosticsStore( - { - read: (key) => - store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, - write: (key, text) => store.write("lib-diagnostics", key, text), - }, - checkerExecutableIdentity(toolchain.ts7Executable), - ); - } compiler = new NativeCompiler(toolchain, cache); return compiler; }; From a8484aea07e84bef367990532c70ba607d171943 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:22:01 -0500 Subject: [PATCH 164/169] Address review feedback on request-body streaming --- .../compiler/shims/hono-node-server/v2.ts | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index c46813c7b..a4dcc6805 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -142,56 +142,72 @@ function requestHeaders(incoming: IncomingMessage): Headers { * (the package's `readableEnded`) and whether a drain started. */ type IncomingState = { ended: boolean; draining: boolean }; -/** The request body as the package streams it: each pull reads one chunk - * of the incoming message, which pauses between pulls, so the socket - * holds what the app has not read yet. The stream pulls once on its own - * when it is constructed; the package builds its stream only when the app - * reads the body, so until the app's first read a drain discards the - * chunks instead of pausing for them. */ +/** The incoming message's readableHighWaterMark: Readable.toWeb queues + * this many bytes before it pauses the message. */ +const BODY_HIGH_WATER_MARK = 64 * 1024; + +/** The request body as the package streams it: Readable.toWeb over the + * incoming message, read one chunk per pull. Chunks queue until + * BODY_HIGH_WATER_MARK bytes wait unread, then the message pauses until a + * pull takes them, so the socket holds the rest. The stream pulls once on + * its own when it is constructed; the package builds its stream only when + * the app reads the body, so until the app's first read a drain discards + * the chunks instead of queueing them. */ function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream { let pulls = 0; let listening = false; - let settled = false; + let ended = false; + let failure: Error | undefined = undefined; + const queue: Buffer[] = []; + let queued = 0; let wake: (() => void) | undefined = undefined; const woken = (): void => { const resolve = wake; wake = undefined; if (resolve !== undefined) resolve(); }; + const listen = (): void => { + listening = true; + incoming.on("data", (chunk: Buffer) => { + if (state.draining && pulls === 1) return; + queue.push(chunk); + queued += chunk.length; + if (queued >= BODY_HIGH_WATER_MARK) incoming.pause(); + woken(); + }); + incoming.on("end", () => { + ended = true; + woken(); + }); + incoming.on("error", (err: Error) => { + failure = err; + woken(); + }); + }; return new ReadableStream({ - pull(controller) { + async pull(controller) { pulls++; - return new Promise((resolve) => { - if (settled) { - resolve(); - return; - } - wake = resolve; - if (listening) { - incoming.resume(); - return; - } - listening = true; - incoming.on("data", (chunk: Buffer) => { - if (state.draining && pulls === 1) return; - incoming.pause(); - if (settled) return; + if (!listening) listen(); + for (;;) { + const chunk = queue.shift(); + if (chunk !== undefined) { + queued -= chunk.length; + if (queued < BODY_HIGH_WATER_MARK) incoming.resume(); controller.enqueue(chunk); - woken(); - }); - incoming.on("end", () => { - if (settled) return; - settled = true; + return; + } + if (failure !== undefined) { + controller.error(failure); + return; + } + if (ended) { controller.close(); - woken(); + return; + } + await new Promise((resolve) => { + wake = resolve; }); - incoming.on("error", (err: Error) => { - if (settled) return; - settled = true; - controller.error(err); - woken(); - }); - }); + } }, }); } From 61bae8c5e077099b4fa0f00be254c2df0d395d8d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:30:24 -0500 Subject: [PATCH 165/169] Keep package-created values of interfaces extending package interfaces dynamic The declaration boundary projected a user-declared interface as plain data even when it extends a package interface. effect's Request.tagged constructors create such values (interface Double extends Request.Request) with a prototype, so the boundary cast threw "expected a plain object (this value has a prototype)". A user interface now projects only when every interface it extends is user-declared too; other values keep their JavaScript-world typing, so the package finds its prototype members when the value goes back to it. --- .../compiler/src/frontend/decl-projection.ts | 17 +++++++++++++++-- .../cases/types-boundary-prototype/main.ts.json | 6 ++++++ .../npm/cases/types-boundary-prototype/main.ts | 13 +++++++++++++ .../node_modules/reqtag/index.d.ts | 8 ++++++++ .../node_modules/reqtag/index.js | 15 +++++++++++++++ .../node_modules/reqtag/package.json | 1 + .../cases/types-boundary-prototype/package.json | 1 + 7 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/main.ts create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/package.json diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index f2340e984..eaf7486df 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -252,6 +252,16 @@ class TypePrinter { return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@")); } + /** Whether an interface type and every type it extends are declared in + * user files. */ + private userInterface(type: ts.Type, seen = new Set()): boolean { + if (!declaredInUserFile(type.getSymbol())) return false; + const target = type.isTypeReference() ? type.getTarget() : type; + if (target === undefined || !target.isClassOrInterface() || seen.has(target)) return true; + seen.add(target); + return this.checker.getBaseTypes(target).every((base) => this.userInterface(base, seen)); + } + private printObject(type: ts.Type, depth: number): string | null { const checker = this.checker; if (checker.isArrayType(type)) { @@ -280,8 +290,11 @@ class TypePrinter { if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method)) return null; // Interfaces: only user-declared ones describe plain data reliably - // (package and lib interfaces often describe class instances). - if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null; + // (package and lib interfaces often describe class instances), and + // only when every interface they extend is user-declared too: a user + // interface extending a package one (effect's Request.Request) + // inherits members the package's own constructors create. + if (symbol.flags & ts.SymbolFlags.Interface && !this.userInterface(type)) return null; } if ( checker.getCallSignatures(type).length > 0 || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json new file mode 100644 index 000000000..2abe1bd8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-boundary-prototype/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/main.ts b/tests/fixtures/npm/cases/types-boundary-prototype/main.ts new file mode 100644 index 000000000..6645535e4 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/main.ts @@ -0,0 +1,13 @@ +// A user interface extending a package interface types values the +// package's constructors create with a prototype (effect's Request.tagged +// pattern): they stay JavaScript values, so the package still finds its +// prototype members when the value goes back to it. +import { run, tagged, type Req } from "reqtag"; + +interface Double extends Req { + readonly _tag: "Double"; + readonly value: number; +} +const Double = tagged("Double"); +const request = Double({ value: 21 }); +console.log(run(request), request.value * 2, request._tag); diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts new file mode 100644 index 000000000..731b6a07f --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts @@ -0,0 +1,8 @@ +export declare const ReqTypeId: unique symbol; +export interface Req { + readonly [ReqTypeId]: { readonly _A: A }; +} +export declare function tagged & { readonly _tag: string }>( + tag: R["_tag"], +): (args: Omit) => R; +export declare function run(request: Req): string; diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js new file mode 100644 index 000000000..196bf555e --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js @@ -0,0 +1,15 @@ +// Requests share a prototype that carries a symbol-keyed marker and a +// method, as effect's Request.tagged constructors do. +export const ReqTypeId = Symbol.for("reqtag/Req"); +const proto = { + [ReqTypeId]: true, + describe() { + return `${this._tag}(${this.value})`; + }, +}; +export function tagged(tag) { + return (args) => Object.assign(Object.create(proto), args, { _tag: tag }); +} +export function run(request) { + return request[ReqTypeId] === true ? request.describe() : "not a request"; +} diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json new file mode 100644 index 000000000..e461981ee --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json @@ -0,0 +1 @@ +{ "name": "reqtag", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/package.json b/tests/fixtures/npm/cases/types-boundary-prototype/package.json new file mode 100644 index 000000000..ac2988811 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/package.json @@ -0,0 +1 @@ +{ "name": "types-boundary-prototype-case", "private": true, "type": "module" } From 40c3f69b4091590c9840002157b1114ffb5e66cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:31:21 -0500 Subject: [PATCH 166/169] Read an AbortSignal's EventTarget methods as EventTarget's own An AbortSignal answered addEventListener, removeEventListener and dispatchEvent read as values with functions of its own, which only ran on AbortSignal handles: calling one on a plain EventTarget threw ERR_INVALID_THIS. In Node they are EventTarget.prototype's methods, shared with every target. The signal's property read now answers the EventTarget method values (scr_event_target_method_value), which run on any EventTarget receiver, AbortSignal included, so signal.addEventListener === new EventTarget().addEventListener holds. The signal's own method table keeps throwIfAborted. --- packages/runtime/src/scr_event_target.c | 4 ++++ packages/runtime/src/scr_event_target.h | 5 +++++ packages/runtime/src/scr_fetch.c | 22 ++++++++-------------- tests/corpus/7740-handle-method-values.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 7c8a465a6..597eeca9b 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1054,6 +1054,10 @@ static ScrDyn *scr_et_method_value(const char *key, size_t len, bool target) { return NULL; } +ScrDyn *scr_event_target_method_value(const char *key, size_t len) { + return scr_et_method_value(key, len, true); +} + static ScrDyn *scr_event_target_value(const ScrEvent *ev) { return ev->target ? scr_dyn_retain(ev->target) : scr_dyn_new_null(); } diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h index e787cdbdb..e1ff5d0da 100644 --- a/packages/runtime/src/scr_event_target.h +++ b/packages/runtime/src/scr_event_target.h @@ -56,6 +56,11 @@ void scr_et_core_drop(ScrEventTargetCore *core); ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, size_t argc); +/* EventTarget.prototype's method `key` (addEventListener, + * removeEventListener, dispatchEvent) as a function value (+1), the one + * every EventTarget answers, AbortSignal included: it runs on any target + * its `this` is. NULL for another name. */ +ScrDyn *scr_event_target_method_value(const char *key, size_t len); /* Dispatches an event handle (borrowed) without the argument checks. */ void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event); /* `on` attributes (Node's defineEventHandler): get answers +1 (null diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 916fc6bf9..c8847f335 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -713,7 +713,8 @@ static ScrDyn *sf_signal_get(void *ptr, const char *key, size_t len) { : scr_dyn_retain(scr_dyn_undefined()); } if (sf_name(key, len, "onabort")) return scr_et_handler_get(&s->core, "abort"); - return NULL; + /* Inherited from EventTarget: the function every target shares. */ + return scr_event_target_method_value(key, len); } static bool sf_signal_set(void *ptr, const char *key, size_t len, @@ -5496,13 +5497,10 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, } /* The members invoke answers, as values (ScrDynHandleMethod), with Node's - * lengths and the brand check each one runs on another receiver. */ + * lengths and the brand check each one runs on another receiver. The + * signal's EventTarget methods are EventTarget's own (sf_signal_get). */ static const ScrDynHandleMethod sf_signal_methods[] = { - {"throwIfAborted", 0, false, - "Value of \"this\" must be of type AbortSignal"}, - {"addEventListener", 2, false}, - {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, - {NULL, 0, false}}; + {"throwIfAborted", 0, false}, {NULL, 0, false}}; static const ScrDynHandleMethod sf_stream_methods[] = { {"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}}; static const ScrDynHandleMethod sf_reader_methods[] = { @@ -5529,7 +5527,7 @@ static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, .methods = sf_signal_methods, - .invalid_this = "Value of \"this\" must be of type EventTarget"}; + .invalid_this = "Value of \"this\" must be of type AbortSignal"}; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL}; @@ -5613,16 +5611,12 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *valu return false; } static const ScrDynHandleMethod sf_signal_methods[] = { - {"throwIfAborted", 0, false, - "Value of \"this\" must be of type AbortSignal"}, - {"addEventListener", 2, false}, - {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, - {NULL, 0, false}}; + {"throwIfAborted", 0, false}, {NULL, 0, false}}; static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, .methods = sf_signal_methods, - .invalid_this = "Value of \"this\" must be of type EventTarget" }; + .invalid_this = "Value of \"this\" must be of type AbortSignal" }; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL }; diff --git a/tests/corpus/7740-handle-method-values.js b/tests/corpus/7740-handle-method-values.js index 3694c204f..dffcba306 100644 --- a/tests/corpus/7740-handle-method-values.js +++ b/tests/corpus/7740-handle-method-values.js @@ -85,6 +85,22 @@ async function receivers() { } catch (e) { report("foreign addEventListener", e); } + // The signal's EventTarget methods are EventTarget's own: one function + // shared with every target, which runs on any EventTarget receiver. + const target = untyped(new EventTarget()); + const add = signal.s.addEventListener; + console.log(add === target.addEventListener, signal.s.dispatchEvent === target.dispatchEvent); + console.log(add.name, add.length, "addEventListener" in signal.s, "throwIfAborted" in target); + const heard = (event) => console.log("heard", event.type, event.target === target); + add.call(target, "ping", heard); + console.log(signal.s.dispatchEvent.call(target, new Event("ping"))); + signal.s.removeEventListener.call(target, "ping", heard); + target.dispatchEvent(new Event("ping")); + try { + throwIfAborted.call(target); + } catch (e) { + report("target throwIfAborted", e); + } } async function streams() { From d9544bb71d9f908cf82f99e32a7102c3e3b0b7c5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:57:37 -0500 Subject: [PATCH 167/169] Release EventTarget's shared method values at exit when only a signal read them The EventTarget method values are cached for the program, and exit released them only when an EventTarget, an event or a listener had registered the unit's teardown. A program that read an AbortSignal's addEventListener and never added a listener or made a target left the cached functions live, and RC-audit builds exited with 99. Caching a method value now registers the teardown too. --- .../corpus/7741-abort-signal-listener-value.js.json | 6 ++++++ packages/runtime/src/scr_event_target.c | 3 +++ tests/corpus/7741-abort-signal-listener-value.js | 10 ++++++++++ .../7741-abort-signal-listener-value.js.json | 1 + 4 files changed, 20 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json create mode 100644 tests/corpus/7741-abort-signal-listener-value.js create mode 100644 tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json new file mode 100644 index 000000000..c556e243e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7741-abort-signal-listener-value.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 597eeca9b..9ca91879a 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1043,6 +1043,9 @@ static ScrDyn *scr_et_method_value(const char *key, size_t len, bool target) { const char *name = scr_et_method_names[i]; if (strlen(name) != len || memcmp(name, key, len) != 0) continue; if (!scr_et_method_values[i]) { + /* An AbortSignal answers these without the unit ever installing: + * exit releases the cached function either way. */ + scr_et_register_teardown(); ScrClosure *clo = scr_closure_new(NULL, 1); clo->caps[0] = scr_box_new(SCR_BOX_F64); scr_box_set_f64(clo->caps[0], (double)i); diff --git a/tests/corpus/7741-abort-signal-listener-value.js b/tests/corpus/7741-abort-signal-listener-value.js new file mode 100644 index 000000000..1a8a201d7 --- /dev/null +++ b/tests/corpus/7741-abort-signal-listener-value.js @@ -0,0 +1,10 @@ +// An AbortSignal's EventTarget methods read as values, in a program that +// never adds a listener or makes an EventTarget of its own: the shared +// functions are released at exit like every other value. +const holder = JSON.parse("{}"); +holder.controller = new AbortController(); +const signal = holder.controller.signal; +const add = signal.addEventListener; +console.log(typeof add, add.name, add.length, signal.dispatchEvent.name); +console.log("removeEventListener" in signal, signal.removeEventListener.length); +console.log(signal.aborted); diff --git a/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json b/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json new file mode 100644 index 000000000..e028a5bb1 --- /dev/null +++ b/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 7500 } From efc351f202d1636dc455b73a2408a59c400c22ad Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 06:22:00 -0500 Subject: [PATCH 168/169] Read emitter analyses from one recorded walk of the module The LLVM emitter's constructor runs many analyses that each walk every function body to collect facts from a few node kinds. IrNodeIndex (ir/node-index.ts) walks the bodies once, before those analyses, and records each body's nodes in everyStmtList's order with a kind code per node and the kinds present per function. An analysis names the kinds it reads and receives exactly those nodes, in the same order, without recursing through the tree; a function the index does not hold (a rewritten copy) is walked as before. The index also lists the distinct type objects of the module, which the boxed-union and runtime-feature scans read instead of walking every expression's type tree. These read the index: walk borrows, appended array literals, constant callbacks and the rebinding census, int32 slots (dynamic field stores, call facts, the box census, candidate writes and the range walks), initializer bindings, stack observations, the may-throw call-graph census, published types and construction facts, final fields, stable globals, reference writes (private arrays, call targets, definitions, fresh constructors), boxed unions and runtime features. Analyses whose walks track their position (statement lists, nesting, ordered dataflow) keep their own walks. Callers outside the emitter pass no index and walk as before. The emitted LLVM IR is unchanged. packages/compiler/native/cli.profdata.gz is refreshed with scripts/refresh-cli-profile.mjs on this change, so the native compiler's optimization profile covers the rewritten analyses. --- packages/compiler/native/cli.profdata.gz | Bin 1634316 -> 1701590 bytes packages/compiler/src/backend/immutability.ts | 50 ++-- .../backend/llvm/appended-array-literals.ts | 70 +++--- .../src/backend/llvm/constant-callbacks.ts | 122 ++++++---- packages/compiler/src/backend/llvm/emitter.ts | 35 ++- .../src/backend/llvm/nullable-unions.ts | 14 +- .../compiler/src/backend/llvm/receivers.ts | 1 + .../src/backend/llvm/reference-writes.ts | 149 ++++++------ .../compiler/src/backend/llvm/walk-borrows.ts | 86 +++---- packages/compiler/src/backend/may-throw.ts | 38 +++- packages/compiler/src/backend/publication.ts | 27 ++- .../src/backend/stack-observations.ts | 93 +++++--- .../src/ir/callback-specialization.ts | 39 ++-- packages/compiler/src/ir/final-fields.ts | 30 +-- .../compiler/src/ir/function-references.ts | 3 + .../compiler/src/ir/initializer-bindings.ts | 40 ++-- packages/compiler/src/ir/int32-slots.ts | 166 +++++++++++--- packages/compiler/src/ir/node-index.ts | 214 ++++++++++++++++++ packages/compiler/src/ir/runtime-features.ts | 26 ++- packages/compiler/test/node-index.test.ts | 157 +++++++++++++ 20 files changed, 1006 insertions(+), 354 deletions(-) create mode 100644 packages/compiler/src/ir/node-index.ts create mode 100644 packages/compiler/test/node-index.test.ts diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index aa0e9c36beac0cdc3528321a8a654ede3118b4b4..b5f1e079f522bd1acd58a9d574272a2e272a8f2a 100644 GIT binary patch literal 1701590 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

3f&^%Su_N3Dd0UnlS_1D@h_lKMb~=U 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Π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

EUyTCz1;GCpfGvQ(e**sX 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�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 z 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

}dYZ!XoR4hePNvP**WH|J8JZoo%4`CjleW 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

    %ap3}3?b3XgriHGXU;p~K~s((@Cu@Pt%6ej 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 z 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 z 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

    G)uZx3(^uP<+jQ?h!{vH(sy+2B1vHV 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

    4di=<@xe%*eltxQANCDA!5P1fFz$ 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 literal 1634316 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%0Aith&KGvDzCWipZ`M&bXZ;m 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

K{yH){|<+Oo@+RK+o|gnypK-CM2X*Pqn9qA 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 z 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 z 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