From f1d694b47b6169af290d158e9b25d709e2b0cd49 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 18:58:42 -0500 Subject: [PATCH 001/191] 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/191] 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/191] 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/191] 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/191] 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 aeb429c066fec555512e23abb04d5cfd962863bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:45:38 -0500 Subject: [PATCH 006/191] Type empty array literals by their slots, and report package diagnostics accurately - An empty `[]` whose own type is `never[]` (a `return []` in an async function, whose contextual type is `T[] | PromiseLike`, or an object literal's `files: []` field) builds as the unit-only element array, like the empty tuple, so typed array slots receive it through the checked empty-array lift. It was `number[]`, so `string[]` slots refused it with SC1090 or SC2002. - The refusal "instanceof against this native constructor" names its site, and coverage lists the sites whose right-hand side is `Object` or `Function`, which always reach it. An arrow function or method on the right of an untyped `instanceof` throws Node's "Function has non-object prototype" TypeError for an object. - Coverage lists the diagnostics of a function whose signature did not compile. A use of a parameter whose type did not compile reports SC2004. - Coverage lists deferred fences in a module's top-level code under "runs at startup", and the diagnostics envelope marks them `startup: true`. --- docs/content/docs/coverage.mdx | 8 +- docs/content/docs/dependencies.mdx | 2 +- .../compiler/src/backend/llvm/lib-shared.ts | 2 +- packages/compiler/src/coverage/report.ts | 17 ++++- .../compiler/src/diagnostics/diagnostic.ts | 4 + packages/compiler/src/diagnostics/envelope.ts | 4 + .../frontend/lowering/class-construction.ts | 19 ++++- .../lowering/expressions/array-literals.ts | 21 ++++++ .../lowering/expressions/relational.ts | 31 +++++++- .../src/frontend/lowering/lower-functions.ts | 75 +++++++++++++++++++ .../src/frontend/lowering/lower-modules.ts | 34 +++++++++ .../compiler/src/frontend/lowering/lowerer.ts | 20 ++++- packages/compiler/src/frontend/type-mapper.ts | 21 ++++++ .../compiler/src/ir/builtin-signatures.ts | 2 +- .../main.ts.json | 6 ++ .../main.ts.json | 6 ++ packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_json.c | 22 +++++- .../7002-empty-array-contextual-slots/main.ts | 58 ++++++++++++++ .../7005-dyn-constructor-instanceof/main.ts | 11 +++ .../node_modules/tag-types/index.d.ts | 4 + .../node_modules/tag-types/index.js | 61 +++++++++++++++ .../node_modules/tag-types/package.json | 1 + .../blocked-signature-body.ts | 15 ++++ tests/coverage-fixtures/startup-fences.js | 9 +++ .../coverage-js-builtin-callable-alias.txt | 2 +- .../main.ts.json | 1 + .../main.ts.json | 1 + tests/harness/coverage.test.ts | 47 ++++++++++++ 29 files changed, 492 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json create mode 100644 tests/corpus/7002-empty-array-contextual-slots/main.ts create mode 100644 tests/corpus/7005-dyn-constructor-instanceof/main.ts create mode 100644 tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts create mode 100644 tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js create mode 100644 tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json create mode 100644 tests/coverage-fixtures/blocked-signature-body.ts create mode 100644 tests/coverage-fixtures/startup-fences.js create mode 100644 tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json create mode 100644 tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index ae36f736d..954b040db 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -38,6 +38,8 @@ When a program has unsupported operations, the report adds a group for each root compile staticallyStatements that compile to native instructions. in dynamic tierOf those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier. blockersUnsupported operations with counts, diagnostic codes, reasons, locations, and hints. + runs at startupDeferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports. + deferred to runtimeOther deferred operations in JavaScript code. Each throws only if it runs. in unreached codeBlockers in code the entry never reaches. A build never compiles that code, so these cannot fail it. differs from NodeSites that compile but can behave differently from Node.js. See Node divergences. npm package compilationNative package compilation outcomes and refusal reasons. @@ -46,7 +48,7 @@ When a program has unsupported operations, the report adds a group for each root Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see [unsupported code inside a package](/docs/dependencies#unsupported-code-inside-a-package). -If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. +If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports `SC2004` back to the signature. @@ -211,6 +213,10 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost group The id of the root-cause group the diagnostic belongs to. A group's primary site is its first in source order. + + startup + true on a deferred diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise. + diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 85a232bb2..409b9cb30 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -132,7 +132,7 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval ## Unsupported code inside a package -A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. +A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports. ## Provenance sources (experimental) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index c08d7b720..453392243 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -47,7 +47,7 @@ export const LIB_FN_SYMS: Record = { "dyn.fromEntries": "scr_dyn_from_entries", "bytes.constructor": "scr_bytes_constructor", "bytes.construct": "scr_bytes_construct", - "bytes.instanceOf": "scr_bytes_instanceof", + "bytes.instanceOf": "scr_bytes_instanceof_at", "arrayBuffer.constructor": "scr_array_buffer_constructor", "sharedArrayBuffer.new": "scr_shared_array_buffer_new", "sharedArrayBuffer.is": "scr_shared_array_buffer_is", diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 37f77f4c6..79017c97a 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -318,12 +318,23 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions // statements the module carries): they compile, but executing one // throws its SC-coded error — the report lists them so a "compiling" // JS program never hides its dynamic gaps. + // Fences in a module's top-level code run when the module initializes: + // they are listed first, as sites that run at startup. const fences = input.runtimeFences ?? []; - if (fences.length > 0) { + const startup = fences.filter((d) => d.startup === true); + const later = fences.filter((d) => d.startup !== true); + if (startup.length > 0) { out.push( - ` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ` ${c(YELLOW, "runs at startup")} ${startup.length} site${startup.length === 1 ? "" : "s"} ${c(DIM, "(JS statements in module top-level code, which runs when the module loads)")}`, ); - renderRows(groupBlockers(fences, "deferred"), YELLOW); + renderRows(groupBlockers(startup, "deferred"), YELLOW); + out.push(""); + } + if (later.length > 0) { + out.push( + ` ${c(YELLOW, "deferred to runtime")} ${later.length} site${later.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`, + ); + renderRows(groupBlockers(later, "deferred"), YELLOW); out.push(""); } diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 17cb2efc0..625ee93af 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -92,6 +92,10 @@ export interface ScrDiagnostic { * the error may not reproduce under the project's tsc. Machine-readable * output reports these under the "environment" category. */ typeWorld?: boolean; + /** A runtime fence in a module's top-level code, outside every function + * and class member body: it throws when the module initializes, which is + * program startup for a module the entry imports. */ + startup?: true; } /** SC3002–SC3004 — native-helper target, installation, and execution diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index 59c285ead..b4de8db77 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -67,6 +67,9 @@ export interface LocatedDiagnostic extends SourcePosition { hint: string; /** The id of the root-cause group this site belongs to. */ group: string; + /** Deferred scope only: the statement is module top-level code, so it + * runs when its module loads. */ + startup?: true; } export interface DiagnosticGroup { @@ -290,6 +293,7 @@ export function locateDiagnostic( message: diag.message, hint: hintOf(diag), group: groupId(diag, scope), + ...(scope === "deferred" && diag.startup === true ? { startup: true as const } : {}), }; } diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 30f504b4a..398029474 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -24,16 +24,21 @@ const PREFIX = "%class.construct:"; const PACKED = `${PREFIX}packed`; const INSTANCEOF = "%class.instanceof:dynamic"; +/** `value instanceof callee` for an untyped constructor value, through one + * shared helper. `refusal` is the site's message for a runtime + * constructor with no instance test. */ export function checkedClassInstanceOf( lowerer: Lowerer, value: IrExpr, callee: IrExpr, + refusal: string, loc: SrcLoc, ): IrExpr { if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) { const params = [ { localId: "value", name: "value", type: DYN }, { localId: "callee", name: "callee", type: DYN }, + { localId: "refusal", name: "refusal", type: STRING }, ]; lowerer.liftedFns.push({ name: INSTANCEOF, @@ -46,7 +51,11 @@ export function checkedClassInstanceOf( value: { kind: "libCall", fn: "bytes.instanceOf", - args: [varRef("value", DYN, loc), varRef("callee", DYN, loc)], + args: [ + varRef("value", DYN, loc), + varRef("callee", DYN, loc), + varRef("refusal", STRING, loc), + ], type: BOOL, loc, }, @@ -56,7 +65,13 @@ export function checkedClassInstanceOf( loc, }); } - return { kind: "call", callee: INSTANCEOF, args: [value, callee], type: BOOL, loc }; + return { + kind: "call", + callee: INSTANCEOF, + args: [value, callee, { kind: "strLit", value: refusal, type: STRING, loc }], + type: BOOL, + loc, + }; } /** Checked construction dispatches to emitted native constructor thunks. */ diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..cb2d86804 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -58,6 +58,22 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return false; } +/** An empty TypeScript literal whose own type is `never[]`: tsc's type for + * `[]` with no contextual array type (`return []` in an async function, + * whose contextual type is `string[] | PromiseLike`; `{ files: [] }`). + * It holds no element, as the empty tuple does, so it builds as the + * unit-only element array, and an array slot receives it through the + * checked empty-array lift. never's f64 element mapping would make it a + * `number[]`, which no `string[]` slot accepts. */ +function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): boolean { + if (expr.elements.nodes.length > 0 || isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; + const own = lowerer.typeOf(expr); + return ( + lowerer.checker.isArrayType(own) && + (lowerer.checker.getTypeArguments(own as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + /** `[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 @@ -80,6 +96,8 @@ export function lowerArrayLiteral( ? lowerer.typeOf(expr) : (ctxType ?? lowerer.typeOf(expr)); let mapped = expected ?? lowerer.mapTypeOf(tsType); + if (expected === undefined && ctxType === undefined && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // Inferred JavaScript arrays can contain unrelated constructors with // structurally equal checker types. Their native class ABIs still differ. if ( @@ -131,11 +149,14 @@ export function lowerArrayLiteral( // the literal's own inferred type is the real tuple. if (!mapped || mapped.kind === "union" || mapped.kind === "dyn") { const ctxUnion = mapped?.kind === "union" ? mapped : null; + const ctxUnmapped = mapped === null; mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); // The never-tainted own type carries no element information here // either (the JS story above) — only a contextual union's single // array arm below may still supply a static home. if (neverTaintedOwnJs) mapped = null; + if (ctxUnmapped && emptyNeverLiteral(lowerer, expr)) + mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // An EMPTY literal in a union slot (`const t: string[] | undefined = // []`, the `[]` default argument against `string[] | undefined`) has // no useful own type — tsc infers `never[]`, whose f64-element diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 61558f008..759687ff0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -49,6 +49,7 @@ import { InternalCompilerError } from "../../../errors.js"; import { tryLowerExpression } from "./try-lower-expression.js"; 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 { foldedStringKeyOf } from "./object-literals.js"; import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js"; @@ -490,7 +491,13 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const constructor = tryLowerExpression(lowerer, expr.right); if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); - return checkedClassInstanceOf(lowerer, value, constructor, loc); + return checkedClassInstanceOf( + lowerer, + value, + constructor, + dynInstanceOfRefusal(lowerer, expr), + loc, + ); } // `this instanceof Glob` in a JavaScript constructor function: the // function's prototype against the value's prototype chain. @@ -2000,3 +2007,25 @@ function lowerRuntimeKeyIn( } return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc }; } + +/** Standard-library constructors the untyped instanceof helper has no + * instance test for: an untyped `x instanceof Object` always refuses. */ +const DYN_INSTANCEOF_REFUSED: ReadonlySet = new Set(["Object", "Function"]); + +/** The SC1090 message `x instanceof C` throws, with the site's position, + * when `C` is a runtime constructor with no instance test. A right-hand + * side that names one of DYN_INSTANCEOF_REFUSED always reaches that + * refusal, so the site is listed with the runtime fences. */ +function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string { + const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor"); + const symbol = lowerer.checker.getSymbolAtLocation(expr.right); + if ( + symbol !== undefined && + lowerer.isStdlibSymbol(symbol) && + DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol)) + ) + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(expr); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; +} diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 653c0d837..ea7d7957b 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,3 +1,4 @@ +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; @@ -17,6 +18,7 @@ import { type IrLocal, type IrParam, type IrStmt, + type IrType, type SrcLoc, VOID, canBoxFuncIntoDyn, @@ -858,3 +860,76 @@ function isBufferCheckOf(e: IrExpr, slotId: string): boolean { e.type.elem === "u8" ); } + +/** The diagnostics of a TypeScript function whose signature did not + * compile, for the coverage remainder: its statements lower for their + * diagnostics only (the remainder's output is discarded), so the report + * lists them with the signature's blocker instead of hiding them behind it. + * Each parameter whose type maps is declared, and a use of any other + * parameter reports SC2004 back to the declaration. The statements already + * count as failed (countSkippedBody), so the pass leaves the stats alone. + * The pass adds diagnostics only: an internal error while it lowers drops + * what it found, and the signature's blocker stands alone as before. */ +export function blockedBodyDiagnostics( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, +): ScrDiagnostic[] { + const body = decl.body; + if (!body || decl.typeParameters || decl.asteriskToken) return []; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return []; + const symbol = declSymbolOf(lowerer, decl); + if (!symbol || lowerer.fnSigsBySymbol.has(symbol)) return []; + const diagsBefore = lowerer.diags.length; + const fencesBefore = lowerer.runtimeFences.length; + const suppressStats = lowerer.suppressStats; + lowerer.suppressStats = true; + let declared: IrType = DYN; + lowersCleanly(lowerer, () => { + declared = lowerer.declaredReturnType(decl, decl); + }); + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + const ctx = newFnCtx(false, null, null, lowerer.bodyReturnType(isAsync, declared)); + ctx.isAsync = isAsync; + lowerer.fnStack.push(ctx); + let internal = false; + try { + for (const param of decl.parameters.nodes) { + if (isThisParameter(param)) continue; + const declaredParam = lowersCleanly(lowerer, () => { + lowerer.declareParams([param], lowerer.paramShapes([param])); + }); + if (declaredParam) continue; + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol) lowerer.blockedBindings.add(symbol); + } + lowersCleanly(lowerer, () => { + lowerer.lowerStmts(body.statements.nodes); + }); + } catch (error) { + if (!(error instanceof InternalCompilerError)) throw error; + internal = true; + } finally { + lowerer.fnStack.pop(); + lowerer.suppressStats = suppressStats; + } + lowerer.runtimeFences.splice(fencesBefore); + const diags = lowerer.diags.splice(diagsBefore); + return internal || diags.some((d) => d.code === "SC9001") ? [] : diags; +} + +/** Runs `lower`, for blockedBodyDiagnostics: false when it poisons, with the + * diagnostics it recorded dropped (a failed mapping reports what the + * signature's blocker already reports). */ +function lowersCleanly(lowerer: Lowerer, lower: () => void): boolean { + const before = lowerer.diags.length; + try { + lower(); + return true; + } catch (error) { + if (!(error instanceof PoisonError)) throw error; + lowerer.diags.splice(before); + return false; + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5b78c7278..5238333ad 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2929,6 +2929,38 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): return { kind: "assign", localId: g.id, value, loc: locOf(stmt) }; } +/** Marks the runtime fences recorded since `fencesBefore` that lie in + * `sf`'s top-level code: they run when the module initializes. A fence + * inside a function, or inside a class member that runs later (a method, + * accessor, constructor or instance field initializer), runs when that + * code is called. */ +function markModuleInitFences(lowerer: Lowerer, sf: ts.SourceFile, fencesBefore: number): void { + for (let i = fencesBefore; i < lowerer.runtimeFences.length; i++) { + const fence = lowerer.runtimeFences[i]!; + if (fence.loc.file !== sf.fileName || runsAfterModuleInit(sf, fence.loc.start)) continue; + lowerer.runtimeFences[i] = { ...fence, startup: true }; + } +} + +/** Whether the code at `pos` sits in a function or a later-running class + * member body. */ +function runsAfterModuleInit(sf: ts.SourceFile, pos: number): boolean { + let later = false; + ts.walkPreorder(sf, (node) => { + if (node.pos > pos || node.end <= pos) return "skip"; + if ( + ts.isFunctionLike(node) || + (ts.isPropertyDeclaration(node) && + !node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) + ) { + later = true; + return "stop"; + } + return undefined; + }); + return later; +} + export function lowerFileInit( lowerer: Lowerer, sf: ts.SourceFile, @@ -2938,6 +2970,7 @@ export function lowerFileInit( const isAsync = lowerer.asyncInitFiles.has(sf); const ctx = newFnCtx(false, null, null, VOID); ctx.isAsync = isAsync; + const fencesBefore = lowerer.runtimeFences.length; lowerer.fnStack.push(ctx); try { const loc0: SrcLoc = { file: sf.fileName, start: 0, end: 0 }; @@ -3288,6 +3321,7 @@ export function lowerFileInit( }; } finally { lowerer.fnStack.pop(); + markModuleInitFences(lowerer, sf, fencesBefore); } } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index edf0a694b..7838213b2 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -444,6 +444,7 @@ import { lowerNestedFunctionDecl, lowerLambda, lowerFunction, + blockedBodyDiagnostics, } from "./lower-functions.js"; import { lowerArrayMethodCall } from "./containers/array-methods.js"; import { type ArrayHofInfo, fuseArrayPipelines } from "./containers/array-pipelines.js"; @@ -1346,6 +1347,7 @@ export function lowerToIr( // travel with them: a failed statement is never left unexplained. const covered: LowerResult = { ...result, + diagnostics: [...result.diagnostics, ...remainder.reachedBlockedBodyDiagnostics], stats: { ...result.stats, statementsDynamic }, unreached: { diagnostics: rem.diagnostics, @@ -7437,7 +7439,10 @@ export class Lowerer { if (sig && !this.wantBody(sig.name)) continue; const fn = this.lowerFunction(decl); if (fn) functions.push(fn); - else if (this.countsSkips()) this.countSkippedBody(decl); + else if (this.countsSkips()) { + this.countSkippedBody(decl); + if (this.remainder) this.reportBlockedBody(decl); + } } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); @@ -7768,6 +7773,19 @@ export class Lowerer { } } + /** Coverage remainder: the body diagnostics of a function whose + * signature blocked (blockedBodyDiagnostics). A function reachable emit + * reached (its signature's diagnostics flushed there) reports them with + * the reached diagnostics; any other reports them as unreached. */ + readonly reachedBlockedBodyDiagnostics: ScrDiagnostic[] = []; + private reportBlockedBody(decl: ts.FunctionDeclaration): void { + const diags = blockedBodyDiagnostics(this, decl); + const symbol = declSymbolOf(this, decl); + if (symbol && this.alreadyFlushed.has(symbol)) + this.reachedBlockedBodyDiagnostics.push(...diags); + else for (const d of diags) this.pushDiag(d); + } + /** Whether run() counts a signature-blocked declaration in * stats.functionsSkipped: whole-program passes and the coverage * remainder do; an externally-gated emit pass leaves the counting to the diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 9abd5a0b0..93349054a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -5731,6 +5731,19 @@ function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { return underConstAssertion(d); } +/** A TypeScript property assignment `name: []` whose type is `never[]`. */ +function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + const d = p.valueDeclaration; + if (d === undefined || !ts.isPropertyAssignment(d)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(d))) return false; + if (!ts.isArrayLiteralExpression(d.initializer) || d.initializer.elements.nodes.length > 0) + return false; + return ( + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; @@ -7767,6 +7780,14 @@ function mapRecordTypeInner( ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } + // A TypeScript property initialized with an empty literal and typed + // `never[]` (`const out = { files: [] }`) holds no element, so it maps + // like the empty tuple: the unit-only element array, which a + // destination's typed array field (`files: string[]`) receives through + // the checked empty-array lift. never's f64 element would make it a + // `number[]` field. + if (pt?.kind === "array" && emptyNeverArrayProp(p, fieldTs, checker)) + pt = arrayOf(unitOnlyUnion(ctx.unions)); if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN; // An `any[]` field (`list: any[]` in an SDK's message type, also // optional or nullable) is the checked-dynamic array, exactly as diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8f5928d12..70ec7d502 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3570,7 +3570,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "weakSet.new": { argTypes: [DYN], result: DYN }, "dyn.fromEntries": { argTypes: [DYN], result: DYN }, "bytes.constructor": { argTypes: [STRING], result: DYN }, - "bytes.instanceOf": { argTypes: [DYN, DYN], result: BOOL }, + "bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL }, "bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN }, /** The ambient receiver — JS `this` in a plain (non-method) function * body: the innermost binding the current firing/dispatch window diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..d22715215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7002-empty-array-contextual-slots/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..313888b49 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7005-dyn-constructor-instanceof/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a427fc9a1..95f251bd4 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -868,6 +868,7 @@ void scr_dyn_proxy_unsupported(const char *operation); ScrDyn *scr_dyn_new_bytes(const ScrBytes *b); ScrDyn *scr_bytes_constructor(const ScrStr *name); bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); +bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal); 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); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 4e21fa397..906be0ccd 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3920,7 +3920,10 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD return -1; } -bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { +/* `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. */ +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); @@ -3962,12 +3965,29 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { scr_dyn_release(prototype); return found; } + bool undefined_prototype = prototype->kind == SCR_DYN_UNDEF; scr_dyn_release(prototype); + /* A function the program compiled with no prototype (an arrow function + * or a method): Node's TypeError for an object, false for a primitive. + * The runtime's own constructors carry "native:" signatures. */ + if (undefined_prototype && (!callee->v.fn.sig || strncmp(callee->v.fn.sig, "native:", 7) != 0)) { + if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_ARR && value->kind != SCR_DYN_FUNC) return false; + static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + } + if (refusal) { + scr_throw_error_msg_code(SCR_ERR_ERROR, refusal->data, refusal->len, "SC1090"); + return false; } static const char message[] = "instanceof against this native constructor is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; } +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { + return scr_bytes_instanceof_at(value, callee, 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); diff --git a/tests/corpus/7002-empty-array-contextual-slots/main.ts b/tests/corpus/7002-empty-array-contextual-slots/main.ts new file mode 100644 index 000000000..ec08a297a --- /dev/null +++ b/tests/corpus/7002-empty-array-contextual-slots/main.ts @@ -0,0 +1,58 @@ +// Empty array literals whose own type is `never[]`: `return []` in an async +// function (its contextual type is `string[] | PromiseLike`) and +// an object literal's `files: []` field returned or passed where typed +// arrays are declared. Each value holds the declared element type. +interface Listing { + files: string[]; + sizes: number[]; +} + +async function readNames(text: string): Promise { + try { + return JSON.parse(text) as string[]; + } catch { + return []; + } +} + +const loadIds = async (fail: boolean): Promise => { + if (fail) return []; + return [1, 2]; +}; + +function emptyListing(): Listing { + const listing = { files: [], sizes: [] }; + return listing; +} + +function index(): Record<"files" | "dirs", string[]> { + const out = { files: [], dirs: [] }; + return out; +} + +function total(listing: Listing): number { + return listing.sizes.reduce((sum, size) => sum + size, 0); +} + +async function main(): Promise { + const names = await readNames("not json"); + names.push("fallback"); + console.log(names, (await readNames('["a","b"]')).join("+")); + const ids = await loadIds(true); + ids.push(7); + console.log(ids, await loadIds(false)); + + const listing = emptyListing(); + listing.files.push("a.txt"); + listing.sizes.push(3, 4); + console.log(listing, total(listing)); + + const dirs = index(); + dirs.dirs.push("src"); + console.log(JSON.stringify(dirs), dirs.files.length); + + const blank = { files: [], sizes: [] }; + console.log(total(blank), JSON.stringify(blank)); +} + +main(); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/main.ts b/tests/corpus/7005-dyn-constructor-instanceof/main.ts new file mode 100644 index 000000000..c32a820c2 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/main.ts @@ -0,0 +1,11 @@ +// A package's own functions, read off an object the checker cannot type, +// on the right of `instanceof`: Node walks the value's prototype chain for +// an ES5 constructor's prototype, at module load and in calls, and throws a +// TypeError for an arrow function, which has no prototype, when the value +// is an object. +import { arrowCheck, describe, rejects, tags } from "tag-types"; + +console.log(tags()); +console.log(describe("{}"), describe("[1]"), describe("null"), describe('"s"')); +console.log(rejects('{"tag":"!x"}')); +console.log(arrowCheck("{}"), arrowCheck("1")); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts new file mode 100644 index 000000000..adeea728e --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts @@ -0,0 +1,4 @@ +export declare function describe(json: string): string; +export declare function tags(): string; +export declare function rejects(json: string): string; +export declare function arrowCheck(json: string): string; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js new file mode 100644 index 000000000..20eee362f --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js @@ -0,0 +1,61 @@ +"use strict"; +// A bundler's lazy module object holding an ES5 constructor: the checker +// cannot type the object, so the constructor is an untyped value where +// `instanceof` tests it. +var types = {}; +var hasTypes; +function requireTypes() { + if (hasTypes) return types; + hasTypes = 1; + function Type(tag, options) { + this.tag = tag; + this.kind = (options && options.kind) || "scalar"; + } + function Schema(list) { + this.list = list; + } + types.Type = Type; + types.Schema = Schema; + types.make = (tag) => new Type(tag); + return types; +} +var loaded = requireTypes(); +var Type = loaded.Type; +var Schema = loaded.Schema; +var make = loaded.make; + +function compileList(list) { + var tags = []; + list.forEach(function (item) { + if (!(item instanceof Type)) throw new Error("not a Type: " + JSON.stringify(item)); + tags.push(item.tag + ":" + item.kind); + }); + return tags; +} +// Runs when the module loads, as js-yaml's default schema does. +var defaults = new Schema(compileList([new Type("!str"), new Type("!seq", { kind: "sequence" })])); + +module.exports.describe = function (json) { + var value = JSON.parse(json); + return [value instanceof Type, value instanceof Schema, defaults instanceof Schema, defaults instanceof Type].join(","); +}; +module.exports.tags = function () { + return defaults.list.join(" "); +}; +function rejects(json) { + try { + compileList([JSON.parse(json)]); + return "accepted"; + } catch (error) { + return error.message; + } +} +module.exports.rejects = rejects; +// An arrow function has no prototype: Node throws a TypeError for an object. +module.exports.arrowCheck = function (json) { + try { + return String(JSON.parse(json) instanceof make); + } catch (error) { + return error.name + ": " + error.message; + } +}; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json new file mode 100644 index 000000000..a469a45f3 --- /dev/null +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/package.json @@ -0,0 +1 @@ +{ "name": "tag-types", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/coverage-fixtures/blocked-signature-body.ts b/tests/coverage-fixtures/blocked-signature-body.ts new file mode 100644 index 000000000..7f8db4eb8 --- /dev/null +++ b/tests/coverage-fixtures/blocked-signature-body.ts @@ -0,0 +1,15 @@ +// Functions whose signatures cannot compile: the report still lists what +// their bodies would report. A use of the blocked parameter names the +// declaration; the body's own unsupported statement reports itself, with +// the reached diagnostics for a function the entry calls and as unreached +// for one nothing calls. +function measure(target: WeakRef, label: string): number { + const extra = new WeakRef({ label }); + console.log(target); + return label.length + (extra === undefined ? 0 : 1); +} +function unused(target: WeakRef): number { + const extra = new WeakRef({ target }); + return extra === undefined ? 0 : 1; +} +console.log(measure(new WeakRef({}), "abc")); diff --git a/tests/coverage-fixtures/startup-fences.js b/tests/coverage-fixtures/startup-fences.js new file mode 100644 index 000000000..22aad4e0a --- /dev/null +++ b/tests/coverage-fixtures/startup-fences.js @@ -0,0 +1,9 @@ +// Deferred fences in a module's top-level code run when the module loads; +// a fence inside a function runs when the function is called. +"use strict"; +var value = JSON.parse("{}"); +console.log(value instanceof Object); +function check(other) { + return other instanceof Object; +} +console.log(check([])); diff --git a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt index 53eac6c3c..6291c291f 100644 --- a/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt +++ b/tests/harness/__snapshots__/coverage-js-builtin-callable-alias.txt @@ -3,7 +3,7 @@ scriptc coverage tests/coverage-fixtures/js-builtin-callable-alias.js statements analyzed 3 compile statically 2 (66%) - deferred to runtime 1 site (JS statements that throw their fence if executed) + runs at startup 1 site (JS statements in module top-level code, which runs when the module loads) ×1 SC1090 a builtin function alias as an escaping value (call 'ext' directly) at tests/coverage-fixtures/js-builtin-callable-alias.js:8:13 hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary) diff --git a/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json new file mode 100644 index 000000000..9495bb65c --- /dev/null +++ b/tests/harness/corpus-costs/7002-empty-array-contextual-slots/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2282, "san": 1173 } diff --git a/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json new file mode 100644 index 000000000..f84e21437 --- /dev/null +++ b/tests/harness/corpus-costs/7005-dyn-constructor-instanceof/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2308, "san": 955 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 86d202b46..40b55b078 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -182,6 +182,53 @@ test("statements of a signature-blocked function count as not static", () => { expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(5); }); +test("the body of a signature-blocked function reports its diagnostics", () => { + const { coverage } = analyze(fixture("blocked-signature-body.ts")); + const sites = (diags: readonly { code: string; message: string }[]) => + diags.map((d) => `${d.code} ${d.message}`).sort(); + // measure is called: its body's diagnostics join the reached ones. + expect(sites(coverage.diagnostics)).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'measure' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // Nothing calls unused: its signature and body report as unreached. + expect(sites(coverage.unreached?.diagnostics ?? [])).toEqual([ + "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", + "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", + ]); + // The bodies' statements still count once each, as not static. + const un = coverage.unreached?.stats; + expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(2); + expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(6); + expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(6); +}); + +test("deferred fences in a module's top-level code report as running at startup", () => { + const { coverage, sourceTexts } = analyze(fixture("startup-fences.js")); + expect( + (coverage.runtimeFences ?? []).map((d) => [d.code, d.loc.start, d.startup === true]), + ).toEqual([ + ["SC1090", expect.any(Number), true], + ["SC1090", expect.any(Number), false], + ]); + const out = report(fixture("startup-fences.js")); + expect(out).toMatch( + /runs at startup {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:13/, + ); + expect(out).toMatch( + /deferred to runtime {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:10/, + ); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); + expect(envelope.diagnostics.map((d) => [d.scope, d.line, d.startup])).toEqual([ + ["deferred", 5, true], + ["deferred", 7, undefined], + ]); +}); + test("a failed unreached JavaScript statement is always explained", () => { const { coverage } = analyze(fixture("unreached-js-fence.js")); const failed = From 44efd4b55bf1fb0b4500699aa5134fd829ef1c18 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:49:11 -0500 Subject: [PATCH 007/191] 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 008/191] 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 009/191] 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 010/191] 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 011/191] 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 012/191] 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 51b9bdca11d21e7ee2b670df375c636c2020b286 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 21:39:25 -0700 Subject: [PATCH 013/191] Compile chalk 5 from a built-in shim with a callable builder class A class whose merged interface carries a call signature stores its call behavior in a '%call' field; calls and function-slot coercions read it. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/chalk/v5.ts | 573 ++++++++++++++++++ packages/compiler/shims/hot-shots/v8.ts | 2 + .../src/frontend/lowering/lower-calls.ts | 25 +- .../compiler/src/frontend/lowering/lowerer.ts | 17 +- .../compiler/src/frontend/package-shims.ts | 49 +- 5 files changed, 657 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/shims/chalk/v5.ts create mode 100644 packages/compiler/shims/hot-shots/v8.ts diff --git a/packages/compiler/shims/chalk/v5.ts b/packages/compiler/shims/chalk/v5.ts new file mode 100644 index 000000000..6599e35f9 --- /dev/null +++ b/packages/compiler/shims/chalk/v5.ts @@ -0,0 +1,573 @@ +// scriptc's built-in implementation of the `chalk` package, 5.x API. +// +// Programs whose installed chalk is 5.x compile this module in its place +// (packages/compiler/src/frontend/package-shims.ts). chalk builds its +// styles as functions whose prototypes it swaps at run time; here a style +// chain is an instance of one class whose style members are cached +// getters, and the instance is callable through its `%call` field (the +// merged interface below gives it chalk's call signature; the lowerer calls +// the field). The output, color-level detection (supports-color 10: +// FORCE_COLOR, the --color/--no-color flags, TTY, TERM, CI variables) and +// the shared, settable `level` follow chalk 5.6 byte for byte. +import * as os from "node:os"; +import * as tty from "node:tty"; + +export type ColorSupportLevel = 0 | 1 | 2 | 3; + +export interface ColorSupport { + level: ColorSupportLevel; + hasBasic: boolean; + has256: boolean; + has16m: boolean; +} + +export type ColorInfo = ColorSupport | false; + +export interface Options { + readonly level?: ColorSupportLevel; +} + +export type ModifierName = + | "reset" + | "bold" + | "dim" + | "italic" + | "underline" + | "overline" + | "inverse" + | "hidden" + | "strikethrough"; + +export type ForegroundColorName = + | "black" + | "red" + | "green" + | "yellow" + | "blue" + | "cyan" + | "magenta" + | "white" + | "gray" + | "grey" + | "blackBright" + | "redBright" + | "greenBright" + | "yellowBright" + | "blueBright" + | "cyanBright" + | "magentaBright" + | "whiteBright"; + +export type BackgroundColorName = + | "bgBlack" + | "bgRed" + | "bgGreen" + | "bgYellow" + | "bgBlue" + | "bgCyan" + | "bgMagenta" + | "bgWhite" + | "bgGray" + | "bgGrey" + | "bgBlackBright" + | "bgRedBright" + | "bgGreenBright" + | "bgYellowBright" + | "bgBlueBright" + | "bgCyanBright" + | "bgMagentaBright" + | "bgWhiteBright"; + +export type ColorName = ForegroundColorName | BackgroundColorName; + +export type Modifiers = ModifierName; +export type ForegroundColor = ForegroundColorName; +export type BackgroundColor = BackgroundColorName; +export type Color = ColorName; + +// ── ansi-styles ────────────────────────────────────────────────────────── + +export const modifierNames: ModifierName[] = [ + "reset", + "bold", + "dim", + "italic", + "underline", + "overline", + "inverse", + "hidden", + "strikethrough", +]; +export const foregroundColorNames: ForegroundColorName[] = [ + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "blackBright", + "gray", + "grey", + "redBright", + "greenBright", + "yellowBright", + "blueBright", + "magentaBright", + "cyanBright", + "whiteBright", +]; +export const backgroundColorNames: BackgroundColorName[] = [ + "bgBlack", + "bgRed", + "bgGreen", + "bgYellow", + "bgBlue", + "bgMagenta", + "bgCyan", + "bgWhite", + "bgBlackBright", + "bgGray", + "bgGrey", + "bgRedBright", + "bgGreenBright", + "bgYellowBright", + "bgBlueBright", + "bgMagentaBright", + "bgCyanBright", + "bgWhiteBright", +]; +export const colorNames: ColorName[] = [ + ...foregroundColorNames, + ...backgroundColorNames, +]; +export const modifiers: Modifiers[] = modifierNames; +export const foregroundColors: ForegroundColor[] = foregroundColorNames; +export const backgroundColors: BackgroundColor[] = backgroundColorNames; +export const colors: Color[] = colorNames; + +const ESC = "\u001B["; + +/** Open and close codes, indexed by style id (the order of the getters). */ +const OPEN_CODES: number[] = [ + 0, 1, 2, 3, 4, 53, 7, 8, 9, + 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96, 97, + 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107, +]; +const CLOSE_CODES: number[] = [ + 0, 22, 22, 23, 24, 55, 27, 28, 29, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, +]; +const VISIBLE = 45; + +function rgbToAnsi256(red: number, green: number, blue: number): number { + if (red === green && green === blue) { + if (red < 8) return 16; + if (red > 248) return 231; + return Math.round(((red - 8) / 247) * 24) + 232; + } + return ( + 16 + + 36 * Math.round((red / 255) * 5) + + 6 * Math.round((green / 255) * 5) + + Math.round((blue / 255) * 5) + ); +} + +function hexToRgb(hex: string): number[] { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex); + if (matches === null) return [0, 0, 0]; + let colorString = matches[0]; + if (colorString.length === 3) { + let doubled = ""; + for (const character of colorString) doubled += character + character; + colorString = doubled; + } + const integer = Number.parseInt(colorString, 16); + return [(integer >> 16) & 0xff, (integer >> 8) & 0xff, integer & 0xff]; +} + +function ansi256ToAnsi(code: number): number { + if (code < 8) return 30 + code; + if (code < 16) return 90 + (code - 8); + let red: number; + let green: number; + let blue: number; + if (code >= 232) { + red = ((code - 232) * 10 + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + const remainder = code % 36; + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = (remainder % 6) / 5; + } + const value = Math.max(red, green, blue) * 2; + if (value === 0) return 30; + let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red)); + if (value === 2) result += 60; + return result; +} + +/** The open code of a color model at a level: offset 0 is the foreground, + * 10 the background (ansi-styles' wrapAnsi16/256/16m). */ +function rgbOpen(level: number, offset: number, red: number, green: number, blue: number): string { + if (level === 3) return `${ESC}${38 + offset};2;${red};${green};${blue}m`; + if (level === 2) return `${ESC}${38 + offset};5;${rgbToAnsi256(red, green, blue)}m`; + return `${ESC}${ansi256ToAnsi(rgbToAnsi256(red, green, blue)) + offset}m`; +} + +/** chalk's ansi256 model emits the 256-color code at every level (only + * the rgb and hex models downsample). */ +function ansi256Open(offset: number, code: number): string { + return `${ESC}${38 + offset};5;${code}m`; +} + +// ── supports-color ─────────────────────────────────────────────────────── + +function hasFlag(flag: string): boolean { + const argv = process.argv; + const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--"; + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf("--"); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +} + +let flagForceColor: number | undefined = undefined; +if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) { + flagForceColor = 0; +} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) { + flagForceColor = 1; +} + +function envForceColor(): number | undefined { + const force = process.env["FORCE_COLOR"]; + if (force !== undefined) { + if (force === "true") return 1; + if (force === "false") return 0; + return force.length === 0 ? 1 : Math.min(Number.parseInt(force, 10), 3); + } + return undefined; +} + +function translateLevel(level: number): ColorInfo { + if (level === 0) return false; + return { + level: level as ColorSupportLevel, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, + }; +} + +function inEnv(name: string): boolean { + return process.env[name] !== undefined; +} + +function detectLevel(streamIsTTY: boolean): number { + const noFlagForceColor = envForceColor(); + if (noFlagForceColor !== undefined) flagForceColor = noFlagForceColor; + const forceColor = flagForceColor; + if (forceColor === 0) return 0; + if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3; + if (hasFlag("color=256")) return 2; + if (inEnv("TF_BUILD") && inEnv("AGENT_NAME")) return 1; + if (!streamIsTTY && forceColor === undefined) return 0; + const min = forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor; + const env = process.env; + const term = env["TERM"]; + if (term === "dumb") return min; + if (process.platform === "win32") { + const osRelease = os.release().split("."); + if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10_586) { + return Number(osRelease[2]) >= 14_931 ? 3 : 2; + } + return 1; + } + if (inEnv("CI")) { + if (inEnv("GITHUB_ACTIONS") || inEnv("GITEA_ACTIONS") || inEnv("CIRCLECI")) return 3; + if ( + inEnv("TRAVIS") || + inEnv("APPVEYOR") || + inEnv("GITLAB_CI") || + inEnv("BUILDKITE") || + inEnv("DRONE") || + env["CI_NAME"] === "codeship" + ) { + return 1; + } + return min; + } + const teamcity = env["TEAMCITY_VERSION"]; + if (teamcity !== undefined) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(teamcity) ? 1 : 0; + } + if (env["COLORTERM"] === "truecolor") return 3; + if (term === "xterm-kitty") return 3; + if (term === "xterm-ghostty") return 3; + if (term === "wezterm") return 3; + const termProgram = env["TERM_PROGRAM"]; + if (termProgram !== undefined) { + const version = Number.parseInt((env["TERM_PROGRAM_VERSION"] || "").split(".")[0]!, 10); + if (termProgram === "iTerm.app") return version >= 3 ? 3 : 2; + if (termProgram === "Apple_Terminal") return 2; + } + // supports-color tests these against `env.TERM`, which is the string + // "undefined" when TERM is unset. + const termText = term === undefined ? "undefined" : term; + if (/-256(color)?$/i.test(termText)) return 2; + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(termText)) return 1; + if (inEnv("COLORTERM")) return 1; + return min; +} + +const stdoutColor: ColorInfo = translateLevel(detectLevel(tty.isatty(1))); +const stderrColor: ColorInfo = translateLevel(detectLevel(tty.isatty(2))); + +export const supportsColor: ColorInfo = stdoutColor; +export const supportsColorStderr: ColorInfo = stderrColor; + +// ── the builder ────────────────────────────────────────────────────────── + +/** One link of a style chain (chalk's styler). */ +class Styler { + open: string; + close: string; + openAll: string; + closeAll: string; + parent: Styler | undefined; + constructor(open: string, close: string, parent: Styler | undefined) { + this.open = open; + this.close = close; + this.parent = parent; + if (parent === undefined) { + this.openAll = open; + this.closeAll = close; + } else { + this.openAll = parent.openAll + open; + this.closeAll = close + parent.closeAll; + } + } +} + +/** The level every builder of one chalk instance shares. */ +class LevelBox { + level: number; + constructor(level: number) { + this.level = level; + } +} + +function stringReplaceAll(string: string, substring: string, replacer: string): string { + let index = string.indexOf(substring); + if (index === -1) return string; + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ""; + do { + returnValue += string.slice(endIndex, index) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + returnValue += string.slice(endIndex); + return returnValue; +} + +function stringEncaseCRLFWithFirstIndex( + string: string, + prefix: string, + postfix: string, + index: number, +): string { + let endIndex = 0; + let returnValue = ""; + do { + const gotCR = string[index - 1] === "\r"; + returnValue += + string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix; + endIndex = index + 1; + index = string.indexOf("\n", endIndex); + } while (index !== -1); + returnValue += string.slice(endIndex); + return returnValue; +} + +function applyStyle(self: ChalkInstance, string: string): string { + if (self.box.level <= 0 || !string) return self.isEmpty ? "" : string; + let styler = self.styler; + if (styler === undefined) return string; + const openAll = styler.openAll; + const closeAll = styler.closeAll; + if (string.includes("\u001B")) { + while (styler !== undefined) { + string = stringReplaceAll(string, styler.close, styler.open); + styler = styler.parent; + } + } + const lfIndex = string.indexOf("\n"); + if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + return openAll + string + closeAll; +} + +function joinArguments(args: unknown[]): string { + let out = ""; + for (let i = 0; i < args.length; i++) { + if (i > 0) out += " "; + const value = args[i]; + if (value !== undefined && value !== null) out += String(value); + } + return out; +} + +/** A chalk instance or one of its style chains: callable (chalk's call + * signature merges in from the interface), with every style a cached + * getter. */ +export interface ChalkInstance { + (...text: unknown[]): string; +} + +export class ChalkInstance { + "%call": (...text: unknown[]) => string; + box: LevelBox; + styler: Styler | undefined; + isEmpty: boolean; + private readonly cache: (ChalkInstance | undefined)[] = []; + + constructor(box: LevelBox, styler: Styler | undefined, isEmpty: boolean, root: boolean) { + this.box = box; + this.styler = styler; + this.isEmpty = isEmpty; + // One unconditional store: the field's definite assignment. + this["%call"] = (...text: unknown[]): string => + root + ? joinArguments(text) + : applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text)); + } + + get level(): ColorSupportLevel { + return this.box.level as ColorSupportLevel; + } + + set level(level: ColorSupportLevel) { + this.box.level = level; + } + + private style(id: number): ChalkInstance { + const cached = this.cache[id]; + if (cached !== undefined) return cached; + let builder: ChalkInstance; + if (id === VISIBLE) { + builder = new ChalkInstance(this.box, this.styler, true, false); + } else { + const styler = new Styler( + `${ESC}${OPEN_CODES[id]!}m`, + `${ESC}${CLOSE_CODES[id]!}m`, + this.styler, + ); + builder = new ChalkInstance(this.box, styler, this.isEmpty, false); + } + while (this.cache.length <= id) this.cache.push(undefined); + this.cache[id] = builder; + return builder; + } + + private model(open: string, background: boolean): ChalkInstance { + const styler = new Styler(open, background ? `${ESC}49m` : `${ESC}39m`, this.styler); + return new ChalkInstance(this.box, styler, this.isEmpty, false); + } + + rgb(red: number, green: number, blue: number): ChalkInstance { + return this.model(rgbOpen(this.box.level, 0, red, green, blue), false); + } + hex(color: string): ChalkInstance { + const [red, green, blue] = hexToRgb(color); + return this.model(rgbOpen(this.box.level, 0, red!, green!, blue!), false); + } + ansi256(index: number): ChalkInstance { + return this.model(ansi256Open(0, index), false); + } + bgRgb(red: number, green: number, blue: number): ChalkInstance { + return this.model(rgbOpen(this.box.level, 10, red, green, blue), true); + } + bgHex(color: string): ChalkInstance { + const [red, green, blue] = hexToRgb(color); + return this.model(rgbOpen(this.box.level, 10, red!, green!, blue!), true); + } + bgAnsi256(index: number): ChalkInstance { + return this.model(ansi256Open(10, index), true); + } + + get reset(): ChalkInstance { return this.style(0); } + get bold(): ChalkInstance { return this.style(1); } + get dim(): ChalkInstance { return this.style(2); } + get italic(): ChalkInstance { return this.style(3); } + get underline(): ChalkInstance { return this.style(4); } + get overline(): ChalkInstance { return this.style(5); } + get inverse(): ChalkInstance { return this.style(6); } + get hidden(): ChalkInstance { return this.style(7); } + get strikethrough(): ChalkInstance { return this.style(8); } + get visible(): ChalkInstance { return this.style(VISIBLE); } + get black(): ChalkInstance { return this.style(9); } + get red(): ChalkInstance { return this.style(10); } + get green(): ChalkInstance { return this.style(11); } + get yellow(): ChalkInstance { return this.style(12); } + get blue(): ChalkInstance { return this.style(13); } + get magenta(): ChalkInstance { return this.style(14); } + get cyan(): ChalkInstance { return this.style(15); } + get white(): ChalkInstance { return this.style(16); } + get blackBright(): ChalkInstance { return this.style(17); } + get gray(): ChalkInstance { return this.style(18); } + get grey(): ChalkInstance { return this.style(19); } + get redBright(): ChalkInstance { return this.style(20); } + get greenBright(): ChalkInstance { return this.style(21); } + get yellowBright(): ChalkInstance { return this.style(22); } + get blueBright(): ChalkInstance { return this.style(23); } + get magentaBright(): ChalkInstance { return this.style(24); } + get cyanBright(): ChalkInstance { return this.style(25); } + get whiteBright(): ChalkInstance { return this.style(26); } + get bgBlack(): ChalkInstance { return this.style(27); } + get bgRed(): ChalkInstance { return this.style(28); } + get bgGreen(): ChalkInstance { return this.style(29); } + get bgYellow(): ChalkInstance { return this.style(30); } + get bgBlue(): ChalkInstance { return this.style(31); } + get bgMagenta(): ChalkInstance { return this.style(32); } + get bgCyan(): ChalkInstance { return this.style(33); } + get bgWhite(): ChalkInstance { return this.style(34); } + get bgBlackBright(): ChalkInstance { return this.style(35); } + get bgGray(): ChalkInstance { return this.style(36); } + get bgGrey(): ChalkInstance { return this.style(37); } + get bgRedBright(): ChalkInstance { return this.style(38); } + get bgGreenBright(): ChalkInstance { return this.style(39); } + get bgYellowBright(): ChalkInstance { return this.style(40); } + get bgBlueBright(): ChalkInstance { return this.style(41); } + get bgMagentaBright(): ChalkInstance { return this.style(42); } + get bgCyanBright(): ChalkInstance { return this.style(43); } + get bgWhiteBright(): ChalkInstance { return this.style(44); } +} + +function optionsLevel(options: Options | undefined): number { + const level = options === undefined ? undefined : options.level; + if (level !== undefined && level !== 0 && !(Number.isInteger(level) && level >= 0 && level <= 3)) { + throw new Error("The `level` option should be an integer from 0 to 3"); + } + if (level === undefined) return stdoutColor === false ? 0 : stdoutColor.level; + return level; +} + +/** `new Chalk(options)`: a fresh chalk instance with its own level. */ +export class Chalk extends ChalkInstance { + constructor(options?: Options) { + super(new LevelBox(optionsLevel(options)), undefined, false, true); + } +} + +const chalk: ChalkInstance = new Chalk(); +export const chalkStderr: ChalkInstance = new Chalk({ + level: stderrColor === false ? 0 : stderrColor.level, +}); + +export default chalk; diff --git a/packages/compiler/shims/hot-shots/v8.ts b/packages/compiler/shims/hot-shots/v8.ts new file mode 100644 index 000000000..2c036408e --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8.ts @@ -0,0 +1,2 @@ +export class StatsD {} +export default StatsD; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 39b7207c0..53971c7b0 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1619,7 +1619,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } const accessorTarget = lowerer.fieldTarget(expr.expression); if (accessorTarget?.container === "accessor") { - const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); + let callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression); + // A getter returning a callable class instance (chalk's + // `chalk.red("x")`): call through its `%call` field. + if (callee.type.kind === "object") { + callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "func") return lowerCallThroughValue(lowerer, expr, callee, locOf(expr)); + } if (callee.type.kind === "func") { const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr)); return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) }; @@ -1871,7 +1877,19 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // a non-func IR type was already rejected while lowering the callee. // HYBRID (function-with-properties) values call through their %call slot. let callee = lowerer.lowerExpr(expr.expression); - if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "record" || callee.type.kind === "object") + callee = lowerer.hybridCallUnwrap(callee); + return lowerCallThroughValue(lowerer, expr, callee, loc); +} + +/** A call through an evaluated callee value: checked-dynamic callees, + * function unions, typed-rest and runtime-arity closures. */ +function lowerCallThroughValue( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: IrExpr, + loc: SrcLoc, +): IrExpr { // A CHECKED-DYNAMIC callee — `fn(a, b)` where fn is an implicit-any // JS binding (the mustCall body's `fn(...args)`), a dyn capture, or a // keyed read off a dyn value: the dynCall boundary. Arguments convert @@ -9181,7 +9199,8 @@ export function lowerRecordFieldCall( // A HYBRID (function-with-properties) field is callable through its // reserved %call slot — `colors.blue("x")` where blue also carries // `.bold` (the chalk shape). - if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); + if (callee.type.kind === "record" || callee.type.kind === "object") + callee = lowerer.hybridCallUnwrap(callee); // Inferred JavaScript option records can retain checked callables in // fields even when the checker only describes those members as any. // TypeScript reaches here for callable library types that are checked diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b02a0a843..b1867be32 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -9294,6 +9294,21 @@ export class Lowerer { * type-mapper.ts's chalk-shape mapping — call paths and func-slot coercions * share it. */ hybridCallUnwrap(expr: IrExpr): IrExpr { + // A callable CLASS instance: a class whose merged interface carries a + // call signature stores its call behavior in a `%call` field (the + // chalk package shim's builder). Calls read the field. + if (expr.type.kind === "object") { + const callType = this.classes.get(expr.type.className)?.fields.get("%call"); + if (callType === undefined || callType.kind !== "func") return expr; + return { + kind: "fieldGet", + obj: expr, + className: expr.type.className, + field: "%call", + type: callType, + loc: expr.loc, + }; + } if (expr.type.kind !== "record") return expr; const shape = this.shapes.get(expr.type.shapeId); const call = shape?.fields.find((f) => f.name === "%call"); @@ -9624,7 +9639,7 @@ export class Lowerer { // A HYBRID (function-with-properties) record flowing into a plain // func slot extracts its reserved %call field — the chalk shape's // "the value IS callable" half (type-mapper.ts's hybrid mapping). - if (expected.kind === "func" && expr.type.kind === "record") { + if (expected.kind === "func" && (expr.type.kind === "record" || expr.type.kind === "object")) { const un = this.hybridCallUnwrap(expr); if (un !== expr && typeEquals(un.type, expected)) return un; } diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 86a41d3a6..f9cf1deb6 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -62,6 +62,9 @@ interface ImplementationShim { * of it (a __commonJS chunk whose key names the package, its version * and this file) is the package itself. */ mainFile: string; + /** The package's declaration entry relative to its directory, when it + * is not `index.d.ts`. */ + typesFile?: string; } const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ @@ -79,6 +82,26 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ ]), }, ], + [ + "chalk", + { + name: "chalk", + defaultExport: true, + mainFile: "source/index.js", + typesFile: "source/index.d.ts", + versions: new Map([[5, "chalk/v5.ts"]]), + }, + ], + [ + "hot-shots", + { + name: "hot-shots", + defaultExport: true, + mainFile: "index.js", + typesFile: "types.d.ts", + versions: new Map([[8, "hot-shots/v8.ts"]]), + }, + ], [ "undici", { @@ -280,16 +303,32 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean { return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null; } +/** 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 (path.endsWith("/index.d.ts")) { + const pkgDir = dirname(path); + const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1)); + if (shim !== undefined && shim.typesFile === undefined) return { pkgDir, shim }; + } + for (const shim of IMPLEMENTATION_SHIMS.values()) { + if (shim.typesFile === undefined) continue; + const suffix = `/${shim.name}/${shim.typesFile}`; + if (path.endsWith(suffix)) return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim }; + } + return null; +} + /** The fs-shadow answer for a shimmed package's declaration entry: the * installed `index.d.ts` reads as a re-export of the version's shim module, * so the checker types each importer against its own version. Undefined * for every other path. */ export function packageShimDeclarationText(path: string): string | undefined { - if (!path.endsWith("/index.d.ts")) return undefined; - const pkgDir = dirname(path); - const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1); - const shim = IMPLEMENTATION_SHIMS.get(name); - if (shim === undefined) return undefined; + const located = shimOfDeclarationPath(path); + if (located === null) return undefined; + const { pkgDir, shim } = located; + const name = shim.name; // A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no // declarations. Nothing at its path names the installed runtime version, // so it reads as the shim only when every supported version shares one From 750c817649e01371c160e91ee5b5263c671440ef Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 21:45:50 -0700 Subject: [PATCH 014/191] Compile hot-shots 8 from a built-in typed StatsD shim udp over node:dgram (TypeScript may use the send completion-callback form), tcp over node:net, mock mode, child clients, tags, checks and events; uds reports the missing unix-dgram addon as hot-shots does. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/hot-shots/v8.ts | 952 +++++++++++++++++- .../src/frontend/lowering/lower-dgram.ts | 4 +- 2 files changed, 954 insertions(+), 2 deletions(-) diff --git a/packages/compiler/shims/hot-shots/v8.ts b/packages/compiler/shims/hot-shots/v8.ts index 2c036408e..a8af7a19e 100644 --- a/packages/compiler/shims/hot-shots/v8.ts +++ b/packages/compiler/shims/hot-shots/v8.ts @@ -1,2 +1,952 @@ -export class StatsD {} +// scriptc's built-in implementation of the `hot-shots` package, 8.x API. +// +// Programs whose installed hot-shots is 8.x compile this module in its +// place (packages/compiler/src/frontend/package-shims.ts). It is a typed +// port of hot-shots 8.5's StatsD client: the same datagrams, callbacks, +// error messages and child-client sharing, over node:dgram (udp) and +// node:net (tcp). The uds protocol needs the optional `unix-dgram` addon; +// this entry serves programs where it is not installed, so uds reports +// hot-shots' "not installed" error exactly as Node does. +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import * as net from "node:net"; +import * as os from "node:os"; + +export type Tags = { [key: string]: string } | string[]; + +export interface ClientOptions { + bufferFlushInterval?: number; + bufferHolder?: { buffer: string }; + cacheDns?: boolean; + cacheDnsTtl?: number; + errorHandler?: (err: Error) => void; + globalTags?: Tags; + globalize?: boolean; + host?: string; + isChild?: boolean; + maxBufferSize?: number; + mock?: boolean; + path?: string; + port?: number; + prefix?: string; + protocol?: "tcp" | "udp" | "uds"; + sampleRate?: number; + suffix?: string; + telegraf?: boolean; + useDefaultRoute?: boolean; + tagPrefix?: string; + tagSeparator?: string; + udsGracefulErrorHandling?: boolean; + udsGracefulRestartRateLimit?: number; + closingFlushInterval?: number; +} + +export interface ChildClientOptions { + globalTags?: Tags; + prefix?: string; + suffix?: string; + errorHandler?: (err: Error) => void; +} + +export interface CheckOptions { + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + message?: string; +} + +export interface DatadogChecks { + OK: 0; + WARNING: 1; + CRITICAL: 2; + UNKNOWN: 3; +} + +export type DatadogChecksValues = 0 | 1 | 2 | 3; + +export interface EventOptions { + aggregation_key?: string; + alert_type?: "info" | "warning" | "success" | "error"; + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + priority?: "low" | "normal"; + source_type_name?: string; +} + +/** A send's completion: hot-shots passes `null` on success. */ +export type StatsCb = (error: Error | null | undefined, bytes?: any) => void; + +type SampleArg = number | Tags | StatsCb | undefined; +type TagsArg = Tags | StatsCb | undefined; + +const PROTOCOL_TCP = "tcp"; +const PROTOCOL_UDS = "uds"; +const PROTOCOL_UDP = "udp"; +const UDS_DEFAULT_GRACEFUL_RESTART_LIMIT = 1000; +const CACHE_DNS_TTL_DEFAULT = 60000; + +function udsErrorCodes(): number[] { + if (process.platform === "linux") { + return [os.constants.errno.ENOTCONN, os.constants.errno.ECONNREFUSED]; + } + if (process.platform === "darwin") { + return [os.constants.errno.EDESTADDRREQ, os.constants.errno.ECONNRESET]; + } + return []; +} +const UDS_ERROR_CODES = udsErrorCodes(); + +// ── helpers ────────────────────────────────────────────────────────────── + +function sanitizeTags(value: string, telegraf: boolean): string { + const blacklist = telegraf ? /:|\||,/g : /:|\||@|,/g; + return String(value).replace(blacklist, "_"); +} + +function formatTags(tags: Tags, telegraf: boolean): string[] { + if (Array.isArray(tags)) return tags; + const out: string[] = []; + for (const key of Object.keys(tags)) { + out.push(`${sanitizeTags(key, telegraf)}:${sanitizeTags(tags[key]!, telegraf)}`); + } + return out; +} + +function isArrayIndexKey(key: string): boolean { + return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; +} + +function overrideTags(parent: string[], child: Tags, telegraf: boolean): string[] { + const childKeys: string[] = []; + const childValues = new Map(); + const toAppend: string[] = []; + for (const tag of formatTags(child, telegraf)) { + const idx = tag.indexOf(":"); + if (idx < 1) { + toAppend.push(tag); + } else { + const key = tag.substring(0, idx); + const value = tag.substring(idx + 1); + let values = childValues.get(key); + if (values === undefined) { + values = []; + childValues.set(key, values); + childKeys.push(key); + } + values.push(value); + } + } + const result = parent.filter((tag) => { + const idx = tag.indexOf(":"); + if (idx < 1) return true; + return !childValues.has(tag.substring(0, idx)); + }); + // hot-shots walks an object's keys: array-index keys ascending first, + // then the rest in insertion order. + const indexKeys = childKeys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b)); + const ordered = indexKeys.concat(childKeys.filter((key) => !isArrayIndexKey(key))); + for (const key of ordered) { + for (const value of childValues.get(key)!) result.push(`${key}:${value}`); + } + return result.concat(toAppend); +} + +function formatDate(date: number): number { + return Math.round(date); +} + +function errorWithCode(message: string, code: string | undefined): Error { + const error: NodeJS.ErrnoException = new Error(message); + error.code = code; + return error; +} + +// ── transports ─────────────────────────────────────────────────────────── + +type SendCallback = (err: Error | null, bytes?: number) => void; + +/** The socket a client sends through (hot-shots' transport interface). */ +interface Transport { + createdAt: number; + send(buf: Buffer, callback: SendCallback): void; + close(): void; + onError(listener: (err: Error) => void): void; + offError(listener: (err: Error) => void): void; + onClose(listener: () => void): void; +} + +/** A socket's 'error' listeners, kept by the transport (the typed + * sockets have no removeListener): one socket listener dispatches to a + * snapshot, and an error nobody listens for throws, as an unhandled + * 'error' event does. */ +class ErrorListeners { + private listeners: ((err: Error) => void)[] = []; + add(listener: (err: Error) => void): void { + this.listeners.push(listener); + } + remove(listener: (err: Error) => void): void { + const index = this.listeners.indexOf(listener); + if (index !== -1) this.listeners.splice(index, 1); + } + emit(err: Error): void { + const snapshot = this.listeners.slice(); + if (snapshot.length === 0) throw err; + for (const listener of snapshot) listener(err); + } +} + +function addEol(buf: Buffer): string { + let msg = buf.toString(); + if (msg.length > 0 && msg[msg.length - 1] !== "\n") msg += "\n"; + return msg; +} + +class UdpTransport implements Transport { + createdAt = 0; + private readonly socket: dgram.Socket; + private readonly errors = new ErrorListeners(); + private resolvedAddress: string | undefined = undefined; + private resolvedAt = 0; + private readonly host: string | undefined; + private readonly port: number; + private readonly cacheDns: boolean; + private readonly cacheDnsTtl: number; + constructor(host: string | undefined, port: number, cacheDns: boolean, cacheDnsTtl: number) { + this.host = host; + this.port = port; + this.cacheDns = cacheDns; + this.cacheDnsTtl = cacheDnsTtl; + this.socket = dgram.createSocket("udp4"); + this.socket.unref(); + this.socket.on("error", (err) => this.errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + if (!this.cacheDns) { + this.socket.send(buf, this.port, this.host || "127.0.0.1", callback); + return; + } + const now = Date.now(); + const cached = this.resolvedAddress; + if (cached === undefined || now - this.resolvedAt > this.cacheDnsTtl) { + dns.lookup(this.host ?? "localhost", (error, address) => { + if (error) { + callback(error); + return; + } + this.resolvedAddress = address; + this.resolvedAt = now; + this.socket.send(buf, this.port, address, callback); + }); + } else { + this.socket.send(buf, this.port, cached, callback); + } + } + close(): void { + this.socket.close(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + onClose(listener: () => void): void { + this.socket.on("close", listener); + } +} + +class TcpTransport implements Transport { + createdAt = 0; + private readonly socket: net.Socket; + private readonly errors = new ErrorListeners(); + constructor(host: string | undefined, port: number) { + this.socket = net.connect(port, host ?? "localhost"); + this.socket.setKeepAlive(true); + this.socket.unref(); + this.socket.on("error", (err) => this.errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + // The typed socket's write takes the data alone; the completion runs + // on the next tick. + this.socket.write(addEol(buf)); + process.nextTick(() => callback(null)); + } + close(): void { + this.socket.destroy(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + onClose(listener: () => void): void { + this.socket.on("close", () => listener()); + } +} + +/** hot-shots reaches Unix datagram sockets through the optional + * `unix-dgram` addon; this entry serves programs without it. */ +function createUdsTransport(): Transport { + throw new Error( + "The library `unix_dgram`, needed for the uds protocol to work, is not installed. " + + "You need to pick another protocol to use hot-shots. " + + "See the hot-shots README for additional details.", + ); +} + +function createTransport(client: StatsD): Transport | null { + let transport: Transport | null = null; + const protocol = client.protocol; + try { + if (protocol === PROTOCOL_TCP) { + transport = new TcpTransport(client.host, client.port); + } else if (protocol === PROTOCOL_UDS) { + transport = createUdsTransport(); + } else if (protocol === PROTOCOL_UDP) { + transport = new UdpTransport(client.host, client.port, client.cacheDns, client.cacheDnsTtl); + } else { + throw new Error(`Unsupported protocol '${protocol}'`); + } + transport.createdAt = Date.now(); + } catch (e) { + const handler = client.errorHandler; + if (handler) { + handler(e as Error); + } else { + console.error(e); + } + } + return transport; +} + +// ── the client ─────────────────────────────────────────────────────────── + +class BufferHolder { + buffer = ""; +} + +/** What a child client inherits from its parent. */ +class ChildInit { + socket: Transport | null; + bufferHolder: BufferHolder; + dnsError: Error | undefined; + constructor(socket: Transport | null, bufferHolder: BufferHolder, dnsError: Error | undefined) { + this.socket = socket; + this.bufferHolder = bufferHolder; + this.dnsError = dnsError; + } +} + +export class StatsD { + protocol: string; + cacheDns: boolean; + cacheDnsTtl: number; + host: string | undefined; + port: number; + path: string | undefined; + prefix: string; + suffix: string; + tagPrefix: string; + tagSeparator: string; + mock: boolean; + globalTags: string[]; + telegraf: boolean; + maxBufferSize: number; + sampleRate: number; + bufferFlushInterval: number; + errorHandler: ((err: Error) => void) | undefined; + udsGracefulErrorHandling: boolean; + udsGracefulRestartRateLimit: number; + isChild: boolean; + closingFlushInterval: number; + mockBuffer: string[] | undefined = undefined; + messagesInFlight = 0; + CHECKS: DatadogChecks = { OK: 0, WARNING: 1, CRITICAL: 2, UNKNOWN: 3 }; + private bufferHolder: BufferHolder; + private transport: Transport | null = null; + private dnsError: Error | undefined = undefined; + private stopFlushInterval: (() => void) | undefined = undefined; + + constructor(options?: ClientOptions, child?: ChildInit) { + const o: ClientOptions = options ?? {}; + const protocol = o.protocol ? o.protocol.toLowerCase() : ""; + this.protocol = protocol ? protocol : PROTOCOL_UDP; + this.cacheDns = o.cacheDns === true; + this.cacheDnsTtl = o.cacheDnsTtl || CACHE_DNS_TTL_DEFAULT; + this.host = o.host || process.env["DD_AGENT_HOST"]; + this.port = o.port || parseInt(process.env["DD_DOGSTATSD_PORT"] ?? "", 10) || 8125; + this.path = o.path; + this.prefix = o.prefix || ""; + this.suffix = o.suffix || ""; + this.tagPrefix = o.tagPrefix || "#"; + this.tagSeparator = o.tagSeparator || ","; + this.mock = o.mock === true; + this.telegraf = o.telegraf || false; + const globalTags = o.globalTags; + this.globalTags = globalTags !== undefined ? formatTags(globalTags, this.telegraf) : []; + const entityId = process.env["DD_ENTITY_ID"]; + if (entityId) { + this.globalTags = this.globalTags.filter( + (item) => item.indexOf("dd.internal.entity_id:") !== 0, + ); + this.globalTags.push("dd.internal.entity_id:".concat(sanitizeTags(entityId, false))); + } + this.maxBufferSize = o.maxBufferSize || 0; + this.sampleRate = o.sampleRate || 1; + this.bufferFlushInterval = o.bufferFlushInterval || 1000; + this.bufferHolder = child !== undefined ? child.bufferHolder : new BufferHolder(); + this.errorHandler = o.errorHandler; + const graceful = o.udsGracefulErrorHandling; + this.udsGracefulErrorHandling = graceful === undefined ? true : graceful; + this.udsGracefulRestartRateLimit = + o.udsGracefulRestartRateLimit || UDS_DEFAULT_GRACEFUL_RESTART_LIMIT; + this.isChild = child !== undefined; + this.closingFlushInterval = o.closingFlushInterval || 50; + if (this.mock) this.mockBuffer = []; + if (child === undefined && this.maxBufferSize > 0) { + const handle = setInterval(() => this.onBufferFlushInterval(), this.bufferFlushInterval); + handle.unref(); + this.stopFlushInterval = () => clearInterval(handle); + } + if (child !== undefined) { + if (child.dnsError) this.dnsError = child.dnsError; + this.transport = child.socket; + } + if (!this.transport) this.transport = createTransport(this); + const handler = o.errorHandler; + const transport = this.transport; + if (transport !== null && child === undefined && handler) transport.onError(handler); + if (transport !== null && o.protocol === PROTOCOL_UDS) this.maybeAddUDSErrorHandler(); + } + + private maybeAddUDSErrorHandler(): void { + if (this.isChild || !this.udsGracefulErrorHandling) return; + const transport = this.transport; + if (transport !== null) transport.onError((err) => this.udsErrorHandler(err)); + } + + private udsErrorHandler(err: Error): void { + const code = Number((err as NodeJS.ErrnoException).code); + if (!UDS_ERROR_CODES.includes(-code)) return; + const socket = this.transport; + if (socket === null) return; + if (!socket.createdAt || Date.now() - socket.createdAt < this.udsGracefulRestartRateLimit) { + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + const newSocket = createTransport(this); + if (newSocket === null) { + const errorMessage = "Could not replace UDS connection with new socket"; + if (handler) { + handler(new Error(errorMessage)); + } else { + console.error(errorMessage); + } + return; + } + socket.close(); + this.transport = newSocket; + this.maybeAddUDSErrorHandler(); + if (handler) { + newSocket.onError(handler); + } else { + newSocket.onError((error) => console.error(`hot-shots UDS error: ${String(error)}`)); + } + } + + // ── stats functions ── + + timing(stat: string | string[], time: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, callback?: StatsCb): void; + timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; + timing( + stat: string | string[], + time: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, time, "ms", sampleRate, tags, callback); + } + + increment(stat: string, tags?: Tags): void; + increment(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, callback?: StatsCb): void; + increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + increment( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + // A non-number second argument of a two-argument call is the tags. + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, amount, "c", sampleRate, tagArg, callback); + } + + decrement(stat: string, tags?: Tags): void; + decrement(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + decrement( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); + } + + histogram(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + histogram( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "h", sampleRate, tags, callback); + } + + distribution(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "d", sampleRate, tags, callback); + } + + gauge(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + gauge( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "g", sampleRate, tags, callback); + } + + set(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + unique(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + check( + name: string, + status: DatadogChecksValues, + options?: CheckOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const check: string[] = ["_sc", this.prefix + name + this.suffix, String(status)]; + const metadata: CheckOptions = options ?? {}; + const happened = metadata.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) check.push(`d:${timestamp}`); + } + if (metadata.hostname) check.push(`h:${metadata.hostname}`); + let mergedTags = this.globalTags; + if (tags !== undefined && typeof tags !== "function") { + mergedTags = overrideTags(mergedTags, tags, this.telegraf); + } + if (mergedTags.length > 0) check.push(`#${mergedTags.join(",")}`); + if (metadata.message) check.push(`m:${metadata.message}`); + let cb = callback; + if (typeof tags === "function" && callback === undefined) cb = tags; + this.sendRaw(check.join("|"), cb); + } + + event( + title: string, + text?: string, + options?: EventOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const msgTitle = String(title ? title : ""); + let msgText = String(text ? text : msgTitle); + msgText = msgText.replace(/\n/g, "\\n"); + let message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; + if (options) { + const happened = options.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) message += `|d:${timestamp}`; + } + if (options.hostname) message += `|h:${options.hostname}`; + if (options.aggregation_key) message += `|k:${options.aggregation_key}`; + if (options.priority) message += `|p:${options.priority}`; + if (options.source_type_name) message += `|s:${options.source_type_name}`; + if (options.alert_type) message += `|t:${options.alert_type}`; + } + let cb = callback; + let tagList: Tags | undefined = undefined; + if (typeof tags === "function") { + if (callback === undefined) cb = tags; + } else { + tagList = tags; + } + this.send(message, tagList, cb); + } + + // ── sending ── + + private sendAll( + stat: string | string[], + value: number | string, + type: string, + sampleRateArg: SampleArg, + tagsArg: TagsArg, + callbackArg: StatsCb | undefined, + ): void { + let sampleRate: number | undefined = undefined; + let tags: Tags | undefined = undefined; + let callback: StatsCb | undefined = callbackArg; + if (typeof sampleRateArg === "number") { + sampleRate = sampleRateArg; + if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + } else if (sampleRateArg !== undefined) { + // A non-number sample rate shifts into the tags, and the tags + // argument into the callback. + if (typeof sampleRateArg === "function") { + callback = sampleRateArg; + } else { + tags = sampleRateArg; + callback = typeof tagsArg === "function" ? tagsArg : undefined; + } + } else if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + + if (!Array.isArray(stat)) { + this.sendStat(stat, value, type, sampleRate, tags, callback); + return; + } + let completed = 0; + let calledback = false; + let sentBytes = 0; + const count = stat.length; + const onSend: StatsCb = (error, bytes) => { + completed += 1; + if (calledback) return; + if (error) { + if (callback !== undefined) { + calledback = true; + callback(error); + } else { + const handler = this.errorHandler; + if (handler) { + calledback = true; + handler(error); + } + } + return; + } + if (bytes) sentBytes += bytes as number; + if (completed === count && callback !== undefined) callback(null, sentBytes); + }; + for (const item of stat) this.sendStat(item, value, type, sampleRate, tags, onSend); + } + + private sendStat( + stat: string, + value: number | string, + type: string, + sampleRateArg: number | undefined, + tags: Tags | undefined, + callback: StatsCb | undefined, + ): void { + let message = `${this.prefix + stat + this.suffix}:${value}|${type}`; + const sampleRate = sampleRateArg || this.sampleRate; + if (sampleRate && sampleRate < 1) { + if (Math.random() < sampleRate) { + message += `|@${sampleRate}`; + } else { + if (callback) callback(undefined); + return; + } + } + this.send(message, tags, callback); + } + + private send(message: string, tags: Tags | undefined, callback: StatsCb | undefined): void { + let mergedTags = this.globalTags; + if (tags !== undefined) mergedTags = overrideTags(mergedTags, tags, this.telegraf); + if (mergedTags.length > 0) { + if (this.telegraf) { + const parts = message.split(":"); + message = `${parts[0]!},${mergedTags.join(",").replace(/:/g, "=")}:${parts.slice(1).join(":")}`; + } else { + message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`; + } + } + this.sendRaw(message, callback); + } + + /** hot-shots' `_send`: the message with its tags. */ + private sendRaw(message: string, callback: StatsCb | undefined): void { + const dnsError = this.dnsError; + if (dnsError) { + if (callback) return callback(dnsError); + const handler = this.errorHandler; + if (handler) return handler(dnsError); + throw dnsError; + } + if (!this.mock) { + if (this.maxBufferSize === 0) { + this.sendMessage(message, callback); + } else { + this.enqueue(message, callback); + } + } else { + const buffer = this.mockBuffer; + if (buffer !== undefined) buffer.push(message); + if (callback) callback(null, 0); + } + } + + private enqueue(message: string, callback: StatsCb | undefined): void { + message += "\n"; + if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { + this.flushQueue(callback); + this.bufferHolder.buffer += message; + } else { + this.bufferHolder.buffer += message; + if (callback) callback(null); + } + } + + private flushQueue(callback?: StatsCb): void { + this.sendMessage(this.bufferHolder.buffer, callback); + this.bufferHolder.buffer = ""; + } + + private sendMessage(message: string, callback: StatsCb | undefined): void { + if (message === "") { + if (callback) callback(null); + return; + } + const socketWasMissing = this.transport === null; + if (socketWasMissing && this.protocol === PROTOCOL_UDS) { + this.transport = createTransport(this); + if (this.transport !== null) this.maybeAddUDSErrorHandler(); + } + const socket = this.transport; + if (socketWasMissing || socket === null) { + const error = new Error("Socket not created properly. Check previous errors for details."); + if (callback) return callback(error); + const handler = this.errorHandler; + if (handler) return handler(error); + console.error(String(error)); + return; + } + const handleCallback = (err: Error | null, bytes?: number): void => { + this.messagesInFlight--; + let errFormatted: Error | null = null; + if (err) { + errFormatted = errorWithCode( + `Error sending hot-shots message: ${String(err)}`, + (err as NodeJS.ErrnoException).code, + ); + if (this.protocol === PROTOCOL_UDS && (callback || this.errorHandler)) { + this.udsErrorHandler(err); + } + } + if (callback) { + callback(errFormatted, bytes); + } else if (errFormatted) { + const handler = this.errorHandler; + if (handler) { + handler(errFormatted); + } else { + console.error(String(errFormatted)); + } + } + }; + try { + this.messagesInFlight++; + socket.send(Buffer.from(message), handleCallback); + } catch (err) { + handleCallback(err as Error); + } + } + + private onBufferFlushInterval(): void { + this.flushQueue(); + } + + close(callback?: (error?: Error) => void): void { + const stop = this.stopFlushInterval; + if (stop !== undefined) stop(); + this.flushQueue((err) => { + if (err) { + if (callback) return callback(err); + console.error(err); + return; + } + let intervalAttempts = 0; + const waitForMessages = setInterval(() => { + intervalAttempts++; + if (intervalAttempts > 10) { + console.log("hot-shots could not clear out messages in flight but closing anyways"); + this.messagesInFlight = 0; + } + if (this.messagesInFlight <= 0) { + clearInterval(waitForMessages); + this.closeSocket(callback); + } + }, this.closingFlushInterval); + }); + } + + private closeSocket(callback: ((error?: Error) => void) | undefined): void { + let handledError = false; + const handleErr = (err: unknown): void => { + const errMessage = `Error closing hot-shots socket: ${String(err)}`; + if (handledError) { + console.error(errMessage); + } else { + handledError = true; + if (callback) { + callback(new Error(errMessage)); + } else { + const handler = this.errorHandler; + if (handler) { + handler(new Error(errMessage)); + } else { + console.error(errMessage); + } + } + } + }; + const socket = this.transport; + if (socket === null) { + handleErr(new TypeError("Cannot read properties of null (reading 'removeListener')")); + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + socket.onError(handleErr); + if (callback) { + socket.onClose(() => { + if (!handledError) callback(undefined); + }); + } + try { + socket.close(); + } catch (err) { + handleErr(err); + } + } + + childClient(options?: ChildClientOptions): StatsD { + const o: ChildClientOptions = options ?? {}; + const childTags = o.globalTags; + return new StatsD( + { + isChild: true, + errorHandler: o.errorHandler || this.errorHandler, + host: this.host, + port: this.port, + tagPrefix: this.tagPrefix, + tagSeparator: this.tagSeparator, + prefix: (o.prefix || "") + this.prefix, + suffix: this.suffix + (o.suffix || ""), + globalize: false, + mock: this.mock, + globalTags: + childTags !== undefined + ? overrideTags(this.globalTags, childTags, this.telegraf) + : this.globalTags, + maxBufferSize: this.maxBufferSize, + bufferFlushInterval: this.bufferFlushInterval, + telegraf: this.telegraf, + protocol: this.protocol === "tcp" ? "tcp" : this.protocol === "uds" ? "uds" : "udp", + closingFlushInterval: this.closingFlushInterval, + }, + new ChildInit(this.transport, this.bufferHolder, this.dnsError), + ); + } +} + export default StatsD; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index aa90fc7c7..0e0ddf975 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -612,8 +612,10 @@ export function lowerDgramMethodCall( const fn: IrLibFn = data.type.kind === "string" ? "dgram.sendStr" : "dgram.sendBytes"; return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc }; } + // TypeScript reaches the ladder for the completion-callback form + // send(msg, port, address, callback) (the hot-shots package shim). if ( - isJsSourceFile(ts.getSourceFileOfNode(call)) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) || args.length === 4) && args.length <= 6 && !args.some(ts.isSpreadElement) ) { From cab61904fd43bfa9d4736e2c9a72b435fd9b9aef Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 21:55:38 -0700 Subject: [PATCH 015/191] Send hot-shots uds metrics through a native Unix datagram socket Where unix-dgram is installed beside hot-shots, the shim's entry sends through runtime AF_UNIX datagram calls with the addon's semantics; without it, uds reports the addon missing as hot-shots does. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/hot-shots/client.ts | 1033 +++++++++++++++++ .../compiler/shims/hot-shots/v8-unix-dgram.ts | 8 + packages/compiler/shims/hot-shots/v8.ts | 956 +-------------- .../compiler/src/backend/llvm/lib-datagram.ts | 4 + .../src/frontend/lowering/lower-calls.ts | 43 + .../src/frontend/lowering/lower-namespaces.ts | 8 +- .../compiler/src/frontend/package-shims.ts | 27 +- .../compiler/src/ir/builtin-signatures.ts | 7 + packages/runtime/src/scr_dgram.c | 70 ++ 9 files changed, 1202 insertions(+), 954 deletions(-) create mode 100644 packages/compiler/shims/hot-shots/client.ts create mode 100644 packages/compiler/shims/hot-shots/v8-unix-dgram.ts diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts new file mode 100644 index 000000000..d0b5c8924 --- /dev/null +++ b/packages/compiler/shims/hot-shots/client.ts @@ -0,0 +1,1033 @@ +// scriptc's built-in implementation of the `hot-shots` package, 8.x API: +// a typed port of hot-shots 8.5's StatsD client with the same datagrams, +// callbacks, error messages and child-client sharing, over node:dgram +// (udp), node:net (tcp) and a native Unix datagram socket (uds). The +// entry modules (v8.ts, v8-unix-dgram.ts) say whether the optional +// unix-dgram addon that hot-shots' uds protocol loads is installed. +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import * as net from "node:net"; +import * as os from "node:os"; + +export type Tags = { [key: string]: string } | string[]; + +export interface ClientOptions { + bufferFlushInterval?: number; + bufferHolder?: { buffer: string }; + cacheDns?: boolean; + cacheDnsTtl?: number; + errorHandler?: (err: Error) => void; + globalTags?: Tags; + globalize?: boolean; + host?: string; + isChild?: boolean; + maxBufferSize?: number; + mock?: boolean; + path?: string; + port?: number; + prefix?: string; + protocol?: "tcp" | "udp" | "uds"; + sampleRate?: number; + suffix?: string; + telegraf?: boolean; + useDefaultRoute?: boolean; + tagPrefix?: string; + tagSeparator?: string; + udsGracefulErrorHandling?: boolean; + udsGracefulRestartRateLimit?: number; + closingFlushInterval?: number; +} + +export interface ChildClientOptions { + globalTags?: Tags; + prefix?: string; + suffix?: string; + errorHandler?: (err: Error) => void; +} + +export interface CheckOptions { + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + message?: string; +} + +export interface DatadogChecks { + OK: 0; + WARNING: 1; + CRITICAL: 2; + UNKNOWN: 3; +} + +export type DatadogChecksValues = 0 | 1 | 2 | 3; + +export interface EventOptions { + aggregation_key?: string; + alert_type?: "info" | "warning" | "success" | "error"; + /** A Unix timestamp; hot-shots also takes a Date, which this shim's + * option records cannot hold yet. */ + date_happened?: number; + hostname?: string; + priority?: "low" | "normal"; + source_type_name?: string; +} + +/** A send's completion: hot-shots passes `null` on success. */ +export type StatsCb = (error: Error | null | undefined, bytes?: any) => void; + +type SampleArg = number | Tags | StatsCb | undefined; +type TagsArg = Tags | StatsCb | undefined; + +const PROTOCOL_TCP = "tcp"; +const PROTOCOL_UDS = "uds"; +const PROTOCOL_UDP = "udp"; +const UDS_DEFAULT_GRACEFUL_RESTART_LIMIT = 1000; +const CACHE_DNS_TTL_DEFAULT = 60000; + +function udsErrorCodes(): number[] { + if (process.platform === "linux") { + return [os.constants.errno.ENOTCONN, os.constants.errno.ECONNREFUSED]; + } + if (process.platform === "darwin") { + return [os.constants.errno.EDESTADDRREQ, os.constants.errno.ECONNRESET]; + } + return []; +} +const UDS_ERROR_CODES = udsErrorCodes(); + +// ── helpers ────────────────────────────────────────────────────────────── + +function sanitizeTags(value: string, telegraf: boolean): string { + const blacklist = telegraf ? /:|\||,/g : /:|\||@|,/g; + return String(value).replace(blacklist, "_"); +} + +function formatTags(tags: Tags, telegraf: boolean): string[] { + if (Array.isArray(tags)) return tags; + const out: string[] = []; + for (const key of Object.keys(tags)) { + out.push(`${sanitizeTags(key, telegraf)}:${sanitizeTags(tags[key]!, telegraf)}`); + } + return out; +} + +function isArrayIndexKey(key: string): boolean { + return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; +} + +function overrideTags(parent: string[], child: Tags, telegraf: boolean): string[] { + const childKeys: string[] = []; + const childValues = new Map(); + const toAppend: string[] = []; + for (const tag of formatTags(child, telegraf)) { + const idx = tag.indexOf(":"); + if (idx < 1) { + toAppend.push(tag); + } else { + const key = tag.substring(0, idx); + const value = tag.substring(idx + 1); + let values = childValues.get(key); + if (values === undefined) { + values = []; + childValues.set(key, values); + childKeys.push(key); + } + values.push(value); + } + } + const result = parent.filter((tag) => { + const idx = tag.indexOf(":"); + if (idx < 1) return true; + return !childValues.has(tag.substring(0, idx)); + }); + // hot-shots walks an object's keys: array-index keys ascending first, + // then the rest in insertion order. + const indexKeys = childKeys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b)); + const ordered = indexKeys.concat(childKeys.filter((key) => !isArrayIndexKey(key))); + for (const key of ordered) { + for (const value of childValues.get(key)!) result.push(`${key}:${value}`); + } + return result.concat(toAppend); +} + +function formatDate(date: number): number { + return Math.round(date); +} + +function errorWithCode(message: string, code: string | undefined): Error { + const error: NodeJS.ErrnoException = new Error(message); + error.code = code; + return error; +} + +// ── transports ─────────────────────────────────────────────────────────── + +type SendCallback = (err: Error | null, bytes?: number) => void; + +/** The socket a client sends through (hot-shots' transport interface). */ +interface Transport { + createdAt: number; + send(buf: Buffer, callback: SendCallback): void; + close(): void; + onError(listener: (err: Error) => void): void; + offError(listener: (err: Error) => void): void; + onClose(listener: () => void): void; +} + +/** A socket's 'error' listeners, kept by the transport (the typed + * sockets have no removeListener): one socket listener dispatches to a + * snapshot, and an error nobody listens for throws, as an unhandled + * 'error' event does. */ +class ErrorListeners { + private listeners: ((err: Error) => void)[] = []; + add(listener: (err: Error) => void): void { + this.listeners.push(listener); + } + remove(listener: (err: Error) => void): void { + const index = this.listeners.indexOf(listener); + if (index !== -1) this.listeners.splice(index, 1); + } + emit(err: Error): void { + const snapshot = this.listeners.slice(); + if (snapshot.length === 0) throw err; + for (const listener of snapshot) listener(err); + } +} + +function addEol(buf: Buffer): string { + let msg = buf.toString(); + if (msg.length > 0 && msg[msg.length - 1] !== "\n") msg += "\n"; + return msg; +} + +class UdpTransport implements Transport { + createdAt = 0; + private readonly socket: dgram.Socket; + private readonly errors = new ErrorListeners(); + private resolvedAddress: string | undefined = undefined; + private resolvedAt = 0; + private readonly host: string | undefined; + private readonly port: number; + private readonly cacheDns: boolean; + private readonly cacheDnsTtl: number; + constructor(host: string | undefined, port: number, cacheDns: boolean, cacheDnsTtl: number) { + this.host = host; + this.port = port; + this.cacheDns = cacheDns; + this.cacheDnsTtl = cacheDnsTtl; + this.socket = dgram.createSocket("udp4"); + this.socket.unref(); + this.socket.on("error", (err) => this.errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + if (!this.cacheDns) { + this.socket.send(buf, this.port, this.host || "127.0.0.1", callback); + return; + } + const now = Date.now(); + const cached = this.resolvedAddress; + if (cached === undefined || now - this.resolvedAt > this.cacheDnsTtl) { + dns.lookup(this.host ?? "localhost", (error, address) => { + if (error) { + callback(error); + return; + } + this.resolvedAddress = address; + this.resolvedAt = now; + this.socket.send(buf, this.port, address, callback); + }); + } else { + this.socket.send(buf, this.port, cached, callback); + } + } + close(): void { + this.socket.close(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + onClose(listener: () => void): void { + this.socket.on("close", listener); + } +} + +class TcpTransport implements Transport { + createdAt = 0; + private readonly socket: net.Socket; + private readonly errors = new ErrorListeners(); + constructor(host: string | undefined, port: number) { + this.socket = net.connect(port, host ?? "localhost"); + this.socket.setKeepAlive(true); + this.socket.unref(); + this.socket.on("error", (err) => this.errors.emit(err)); + } + send(buf: Buffer, callback: SendCallback): void { + // The typed socket's write takes the data alone; the completion runs + // on the next tick. + this.socket.write(addEol(buf)); + process.nextTick(() => callback(null)); + } + close(): void { + this.socket.destroy(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + onClose(listener: () => void): void { + this.socket.on("close", () => listener()); + } +} + +// The runtime's AF_UNIX datagram calls (the unix-dgram addon's binding): +// results >= 0, or a negative errno; send answers 1 when it would block. +declare function scriptcUnixDgramSocket(): number; +declare function scriptcUnixDgramConnect(fd: number, path: string): number; +declare function scriptcUnixDgramSend(fd: number, data: Buffer): number; +declare function scriptcUnixDgramClose(fd: number): number; + +let unixDgramInstalled = false; + +/** Whether the unix-dgram addon is installed beside hot-shots (set by the + * entry module). */ +export function configureUnixDgram(installed: boolean): void { + unixDgramInstalled = installed; +} + +const UDS_PATH_DEFAULT = "/var/run/datadog/dsd.socket"; + +/** unix-dgram's errnoException: " " (the code is the + * negative errno; this Error's code field holds its decimal string). */ +function errnoException(errorno: number, syscall: string): Error { + const error: NodeJS.ErrnoException = new Error(`${syscall} ${errorno}`); + error.errno = errorno; + error.code = String(errorno); + error.syscall = syscall; + return error; +} + +/** hot-shots' uds transport over unix-dgram: a connected, non-blocking + * AF_UNIX datagram socket. Sends complete synchronously, as the addon's + * do. The addon polls the socket for reads, which holds the event loop + * until close; a ref'd timer stands in for that poll. */ +class UdsTransport implements Transport { + createdAt = 0; + private fd: number; + private readonly errors = new ErrorListeners(); + private readonly closeListeners: (() => void)[] = []; + private readonly stopHold: () => void; + constructor(path: string) { + const fd = scriptcUnixDgramSocket(); + if (fd < 0) throw errnoException(fd, "socket"); + this.fd = fd; + const hold = setInterval(() => {}, 2147483647); + this.stopHold = () => clearInterval(hold); + const err = scriptcUnixDgramConnect(fd, path); + if (err < 0) { + // The addon emits the connect failure as 'error' before anyone + // listens, which throws; hot-shots closes the socket and rethrows. + this.close(); + throw errnoException(err, "connect"); + } + } + send(buf: Buffer, callback: SendCallback): void { + const result = scriptcUnixDgramSend(this.fd, buf); + if (result < 0) { + callback(errnoException(result, "send")); + } else if (result === 1) { + const congestion: NodeJS.ErrnoException = new Error("congestion"); + congestion.code = "1"; + callback(congestion); + } else { + callback(null); + } + } + close(): void { + const result = scriptcUnixDgramClose(this.fd); + if (result < 0) throw errnoException(result, "close"); + this.fd = -1; + this.stopHold(); + for (const listener of this.closeListeners.slice()) listener(); + } + onError(listener: (err: Error) => void): void { + this.errors.add(listener); + } + offError(listener: (err: Error) => void): void { + this.errors.remove(listener); + } + onClose(listener: () => void): void { + this.closeListeners.push(listener); + } +} + +/** hot-shots' createUdsTransport: without the unix-dgram addon the + * protocol reports it missing. */ +function createUdsTransport(path: string | undefined): Transport { + if (!unixDgramInstalled) { + throw new Error( + "The library `unix_dgram`, needed for the uds protocol to work, is not installed. " + + "You need to pick another protocol to use hot-shots. " + + "See the hot-shots README for additional details.", + ); + } + return new UdsTransport(path ? path : UDS_PATH_DEFAULT); +} + +function createTransport(client: StatsD): Transport | null { + let transport: Transport | null = null; + const protocol = client.protocol; + try { + if (protocol === PROTOCOL_TCP) { + transport = new TcpTransport(client.host, client.port); + } else if (protocol === PROTOCOL_UDS) { + transport = createUdsTransport(client.path); + } else if (protocol === PROTOCOL_UDP) { + transport = new UdpTransport(client.host, client.port, client.cacheDns, client.cacheDnsTtl); + } else { + throw new Error(`Unsupported protocol '${protocol}'`); + } + transport.createdAt = Date.now(); + } catch (e) { + const handler = client.errorHandler; + if (handler) { + handler(e as Error); + } else { + console.error(e); + } + } + return transport; +} + +// ── the client ─────────────────────────────────────────────────────────── + +class BufferHolder { + buffer = ""; +} + +/** What a child client inherits from its parent. */ +class ChildInit { + socket: Transport | null; + bufferHolder: BufferHolder; + dnsError: Error | undefined; + constructor(socket: Transport | null, bufferHolder: BufferHolder, dnsError: Error | undefined) { + this.socket = socket; + this.bufferHolder = bufferHolder; + this.dnsError = dnsError; + } +} + +export class StatsD { + protocol: string; + cacheDns: boolean; + cacheDnsTtl: number; + host: string | undefined; + port: number; + path: string | undefined; + prefix: string; + suffix: string; + tagPrefix: string; + tagSeparator: string; + mock: boolean; + globalTags: string[]; + telegraf: boolean; + maxBufferSize: number; + sampleRate: number; + bufferFlushInterval: number; + errorHandler: ((err: Error) => void) | undefined; + udsGracefulErrorHandling: boolean; + udsGracefulRestartRateLimit: number; + isChild: boolean; + closingFlushInterval: number; + mockBuffer: string[] | undefined = undefined; + messagesInFlight = 0; + CHECKS: DatadogChecks = { OK: 0, WARNING: 1, CRITICAL: 2, UNKNOWN: 3 }; + private bufferHolder: BufferHolder; + private transport: Transport | null = null; + private dnsError: Error | undefined = undefined; + private stopFlushInterval: (() => void) | undefined = undefined; + + constructor(options?: ClientOptions, child?: ChildInit) { + const o: ClientOptions = options ?? {}; + const protocol = o.protocol ? o.protocol.toLowerCase() : ""; + this.protocol = protocol ? protocol : PROTOCOL_UDP; + this.cacheDns = o.cacheDns === true; + this.cacheDnsTtl = o.cacheDnsTtl || CACHE_DNS_TTL_DEFAULT; + this.host = o.host || process.env["DD_AGENT_HOST"]; + this.port = o.port || parseInt(process.env["DD_DOGSTATSD_PORT"] ?? "", 10) || 8125; + this.path = o.path; + this.prefix = o.prefix || ""; + this.suffix = o.suffix || ""; + this.tagPrefix = o.tagPrefix || "#"; + this.tagSeparator = o.tagSeparator || ","; + this.mock = o.mock === true; + this.telegraf = o.telegraf || false; + const globalTags = o.globalTags; + this.globalTags = globalTags !== undefined ? formatTags(globalTags, this.telegraf) : []; + const entityId = process.env["DD_ENTITY_ID"]; + if (entityId) { + this.globalTags = this.globalTags.filter( + (item) => item.indexOf("dd.internal.entity_id:") !== 0, + ); + this.globalTags.push("dd.internal.entity_id:".concat(sanitizeTags(entityId, false))); + } + this.maxBufferSize = o.maxBufferSize || 0; + this.sampleRate = o.sampleRate || 1; + this.bufferFlushInterval = o.bufferFlushInterval || 1000; + this.bufferHolder = child !== undefined ? child.bufferHolder : new BufferHolder(); + this.errorHandler = o.errorHandler; + const graceful = o.udsGracefulErrorHandling; + this.udsGracefulErrorHandling = graceful === undefined ? true : graceful; + this.udsGracefulRestartRateLimit = + o.udsGracefulRestartRateLimit || UDS_DEFAULT_GRACEFUL_RESTART_LIMIT; + this.isChild = child !== undefined; + this.closingFlushInterval = o.closingFlushInterval || 50; + if (this.mock) this.mockBuffer = []; + if (child === undefined && this.maxBufferSize > 0) { + const handle = setInterval(() => this.onBufferFlushInterval(), this.bufferFlushInterval); + handle.unref(); + this.stopFlushInterval = () => clearInterval(handle); + } + if (child !== undefined) { + if (child.dnsError) this.dnsError = child.dnsError; + this.transport = child.socket; + } + if (!this.transport) this.transport = createTransport(this); + const handler = o.errorHandler; + const transport = this.transport; + if (transport !== null && child === undefined && handler) transport.onError(handler); + if (transport !== null && o.protocol === PROTOCOL_UDS) this.maybeAddUDSErrorHandler(); + } + + private maybeAddUDSErrorHandler(): void { + if (this.isChild || !this.udsGracefulErrorHandling) return; + const transport = this.transport; + if (transport !== null) transport.onError((err) => this.udsErrorHandler(err)); + } + + private udsErrorHandler(err: Error): void { + const code = Number((err as NodeJS.ErrnoException).code); + if (!UDS_ERROR_CODES.includes(-code)) return; + const socket = this.transport; + if (socket === null) return; + if (!socket.createdAt || Date.now() - socket.createdAt < this.udsGracefulRestartRateLimit) { + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + const newSocket = createTransport(this); + if (newSocket === null) { + const errorMessage = "Could not replace UDS connection with new socket"; + if (handler) { + handler(new Error(errorMessage)); + } else { + console.error(errorMessage); + } + return; + } + socket.close(); + this.transport = newSocket; + this.maybeAddUDSErrorHandler(); + if (handler) { + newSocket.onError(handler); + } else { + newSocket.onError((error) => console.error(`hot-shots UDS error: ${String(error)}`)); + } + } + + // ── stats functions ── + + timing(stat: string | string[], time: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; + timing(stat: string | string[], time: number, callback?: StatsCb): void; + timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; + timing( + stat: string | string[], + time: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, time, "ms", sampleRate, tags, callback); + } + + increment(stat: string, tags?: Tags): void; + increment(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + increment(stat: string | string[], value: number, callback?: StatsCb): void; + increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + increment( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + // A non-number second argument of a two-argument call is the tags. + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, amount, "c", sampleRate, tagArg, callback); + } + + decrement(stat: string, tags?: Tags): void; + decrement(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, callback?: StatsCb): void; + decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + decrement( + stat: string | string[], + value?: number | Tags, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + let amount = 1; + let tagArg: TagsArg = tags; + if (typeof value === "number") { + amount = value; + } else if (sampleRate === undefined && tags === undefined && callback === undefined) { + tagArg = value; + } + this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); + } + + histogram(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, callback?: StatsCb): void; + histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + histogram( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "h", sampleRate, tags, callback); + } + + distribution(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, callback?: StatsCb): void; + distribution(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "d", sampleRate, tags, callback); + } + + gauge(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, callback?: StatsCb): void; + gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + gauge( + stat: string | string[], + value: number, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "g", sampleRate, tags, callback); + } + + set(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, callback?: StatsCb): void; + set(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + unique(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, callback?: StatsCb): void; + unique(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: SampleArg, + tags?: TagsArg, + callback?: StatsCb, + ): void { + this.sendAll(stat, value, "s", sampleRate, tags, callback); + } + + check( + name: string, + status: DatadogChecksValues, + options?: CheckOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const check: string[] = ["_sc", this.prefix + name + this.suffix, String(status)]; + const metadata: CheckOptions = options ?? {}; + const happened = metadata.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) check.push(`d:${timestamp}`); + } + if (metadata.hostname) check.push(`h:${metadata.hostname}`); + let mergedTags = this.globalTags; + if (tags !== undefined && typeof tags !== "function") { + mergedTags = overrideTags(mergedTags, tags, this.telegraf); + } + if (mergedTags.length > 0) check.push(`#${mergedTags.join(",")}`); + if (metadata.message) check.push(`m:${metadata.message}`); + let cb = callback; + if (typeof tags === "function" && callback === undefined) cb = tags; + this.sendRaw(check.join("|"), cb); + } + + event( + title: string, + text?: string, + options?: EventOptions, + tags?: Tags | StatsCb, + callback?: StatsCb, + ): void { + if (this.telegraf) { + const err = new Error("Not supported by Telegraf / InfluxDB"); + if (callback) return callback(err); + const handler = this.errorHandler; + if (handler) return handler(err); + throw err; + } + const msgTitle = String(title ? title : ""); + let msgText = String(text ? text : msgTitle); + msgText = msgText.replace(/\n/g, "\\n"); + let message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; + if (options) { + const happened = options.date_happened; + if (happened) { + const timestamp = formatDate(happened); + if (timestamp) message += `|d:${timestamp}`; + } + if (options.hostname) message += `|h:${options.hostname}`; + if (options.aggregation_key) message += `|k:${options.aggregation_key}`; + if (options.priority) message += `|p:${options.priority}`; + if (options.source_type_name) message += `|s:${options.source_type_name}`; + if (options.alert_type) message += `|t:${options.alert_type}`; + } + let cb = callback; + let tagList: Tags | undefined = undefined; + if (typeof tags === "function") { + if (callback === undefined) cb = tags; + } else { + tagList = tags; + } + this.send(message, tagList, cb); + } + + // ── sending ── + + private sendAll( + stat: string | string[], + value: number | string, + type: string, + sampleRateArg: SampleArg, + tagsArg: TagsArg, + callbackArg: StatsCb | undefined, + ): void { + let sampleRate: number | undefined = undefined; + let tags: Tags | undefined = undefined; + let callback: StatsCb | undefined = callbackArg; + if (typeof sampleRateArg === "number") { + sampleRate = sampleRateArg; + if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + } else if (sampleRateArg !== undefined) { + // A non-number sample rate shifts into the tags, and the tags + // argument into the callback. + if (typeof sampleRateArg === "function") { + callback = sampleRateArg; + } else { + tags = sampleRateArg; + callback = typeof tagsArg === "function" ? tagsArg : undefined; + } + } else if (typeof tagsArg === "function") { + callback = tagsArg; + } else { + tags = tagsArg; + } + + if (!Array.isArray(stat)) { + this.sendStat(stat, value, type, sampleRate, tags, callback); + return; + } + let completed = 0; + let calledback = false; + let sentBytes = 0; + const count = stat.length; + const onSend: StatsCb = (error, bytes) => { + completed += 1; + if (calledback) return; + if (error) { + if (callback !== undefined) { + calledback = true; + callback(error); + } else { + const handler = this.errorHandler; + if (handler) { + calledback = true; + handler(error); + } + } + return; + } + if (bytes) sentBytes += bytes as number; + if (completed === count && callback !== undefined) callback(null, sentBytes); + }; + for (const item of stat) this.sendStat(item, value, type, sampleRate, tags, onSend); + } + + private sendStat( + stat: string, + value: number | string, + type: string, + sampleRateArg: number | undefined, + tags: Tags | undefined, + callback: StatsCb | undefined, + ): void { + let message = `${this.prefix + stat + this.suffix}:${value}|${type}`; + const sampleRate = sampleRateArg || this.sampleRate; + if (sampleRate && sampleRate < 1) { + if (Math.random() < sampleRate) { + message += `|@${sampleRate}`; + } else { + if (callback) callback(undefined); + return; + } + } + this.send(message, tags, callback); + } + + private send(message: string, tags: Tags | undefined, callback: StatsCb | undefined): void { + let mergedTags = this.globalTags; + if (tags !== undefined) mergedTags = overrideTags(mergedTags, tags, this.telegraf); + if (mergedTags.length > 0) { + if (this.telegraf) { + const parts = message.split(":"); + message = `${parts[0]!},${mergedTags.join(",").replace(/:/g, "=")}:${parts.slice(1).join(":")}`; + } else { + message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`; + } + } + this.sendRaw(message, callback); + } + + /** hot-shots' `_send`: the message with its tags. */ + private sendRaw(message: string, callback: StatsCb | undefined): void { + const dnsError = this.dnsError; + if (dnsError) { + if (callback) return callback(dnsError); + const handler = this.errorHandler; + if (handler) return handler(dnsError); + throw dnsError; + } + if (!this.mock) { + if (this.maxBufferSize === 0) { + this.sendMessage(message, callback); + } else { + this.enqueue(message, callback); + } + } else { + const buffer = this.mockBuffer; + if (buffer !== undefined) buffer.push(message); + if (callback) callback(null, 0); + } + } + + private enqueue(message: string, callback: StatsCb | undefined): void { + message += "\n"; + if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { + this.flushQueue(callback); + this.bufferHolder.buffer += message; + } else { + this.bufferHolder.buffer += message; + if (callback) callback(null); + } + } + + private flushQueue(callback?: StatsCb): void { + this.sendMessage(this.bufferHolder.buffer, callback); + this.bufferHolder.buffer = ""; + } + + private sendMessage(message: string, callback: StatsCb | undefined): void { + if (message === "") { + if (callback) callback(null); + return; + } + const socketWasMissing = this.transport === null; + if (socketWasMissing && this.protocol === PROTOCOL_UDS) { + this.transport = createTransport(this); + if (this.transport !== null) this.maybeAddUDSErrorHandler(); + } + const socket = this.transport; + if (socketWasMissing || socket === null) { + const error = new Error("Socket not created properly. Check previous errors for details."); + if (callback) return callback(error); + const handler = this.errorHandler; + if (handler) return handler(error); + console.error(String(error)); + return; + } + const handleCallback = (err: Error | null, bytes?: number): void => { + this.messagesInFlight--; + let errFormatted: Error | null = null; + if (err) { + errFormatted = errorWithCode( + `Error sending hot-shots message: ${String(err)}`, + (err as NodeJS.ErrnoException).code, + ); + if (this.protocol === PROTOCOL_UDS && (callback || this.errorHandler)) { + this.udsErrorHandler(err); + } + } + if (callback) { + callback(errFormatted, bytes); + } else if (errFormatted) { + const handler = this.errorHandler; + if (handler) { + handler(errFormatted); + } else { + console.error(String(errFormatted)); + } + } + }; + try { + this.messagesInFlight++; + socket.send(Buffer.from(message), handleCallback); + } catch (err) { + handleCallback(err as Error); + } + } + + private onBufferFlushInterval(): void { + this.flushQueue(); + } + + close(callback?: (error?: Error) => void): void { + const stop = this.stopFlushInterval; + if (stop !== undefined) stop(); + this.flushQueue((err) => { + if (err) { + if (callback) return callback(err); + console.error(err); + return; + } + let intervalAttempts = 0; + const waitForMessages = setInterval(() => { + intervalAttempts++; + if (intervalAttempts > 10) { + console.log("hot-shots could not clear out messages in flight but closing anyways"); + this.messagesInFlight = 0; + } + if (this.messagesInFlight <= 0) { + clearInterval(waitForMessages); + this.closeSocket(callback); + } + }, this.closingFlushInterval); + }); + } + + private closeSocket(callback: ((error?: Error) => void) | undefined): void { + let handledError = false; + const handleErr = (err: unknown): void => { + const errMessage = `Error closing hot-shots socket: ${String(err)}`; + if (handledError) { + console.error(errMessage); + } else { + handledError = true; + if (callback) { + callback(new Error(errMessage)); + } else { + const handler = this.errorHandler; + if (handler) { + handler(new Error(errMessage)); + } else { + console.error(errMessage); + } + } + } + }; + const socket = this.transport; + if (socket === null) { + handleErr(new TypeError("Cannot read properties of null (reading 'removeListener')")); + return; + } + const handler = this.errorHandler; + if (handler) socket.offError(handler); + socket.onError(handleErr); + if (callback) { + socket.onClose(() => { + if (!handledError) callback(undefined); + }); + } + try { + socket.close(); + } catch (err) { + handleErr(err); + } + } + + childClient(options?: ChildClientOptions): StatsD { + const o: ChildClientOptions = options ?? {}; + const childTags = o.globalTags; + return new StatsD( + { + isChild: true, + errorHandler: o.errorHandler || this.errorHandler, + host: this.host, + port: this.port, + tagPrefix: this.tagPrefix, + tagSeparator: this.tagSeparator, + prefix: (o.prefix || "") + this.prefix, + suffix: this.suffix + (o.suffix || ""), + globalize: false, + mock: this.mock, + globalTags: + childTags !== undefined + ? overrideTags(this.globalTags, childTags, this.telegraf) + : this.globalTags, + maxBufferSize: this.maxBufferSize, + bufferFlushInterval: this.bufferFlushInterval, + telegraf: this.telegraf, + protocol: this.protocol === "tcp" ? "tcp" : this.protocol === "uds" ? "uds" : "udp", + closingFlushInterval: this.closingFlushInterval, + }, + new ChildInit(this.transport, this.bufferHolder, this.dnsError), + ); + } +} + +export default StatsD; diff --git a/packages/compiler/shims/hot-shots/v8-unix-dgram.ts b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts new file mode 100644 index 000000000..f4bfb180e --- /dev/null +++ b/packages/compiler/shims/hot-shots/v8-unix-dgram.ts @@ -0,0 +1,8 @@ +// The hot-shots 8.x shim for installations with the optional unix-dgram +// addon: the uds protocol sends through a native Unix datagram socket. +import { configureUnixDgram } from "./client.ts"; + +configureUnixDgram(true); + +export * from "./client.ts"; +export { default } from "./client.ts"; diff --git a/packages/compiler/shims/hot-shots/v8.ts b/packages/compiler/shims/hot-shots/v8.ts index a8af7a19e..0d1871170 100644 --- a/packages/compiler/shims/hot-shots/v8.ts +++ b/packages/compiler/shims/hot-shots/v8.ts @@ -1,952 +1,8 @@ -// scriptc's built-in implementation of the `hot-shots` package, 8.x API. -// -// Programs whose installed hot-shots is 8.x compile this module in its -// place (packages/compiler/src/frontend/package-shims.ts). It is a typed -// port of hot-shots 8.5's StatsD client: the same datagrams, callbacks, -// error messages and child-client sharing, over node:dgram (udp) and -// node:net (tcp). The uds protocol needs the optional `unix-dgram` addon; -// this entry serves programs where it is not installed, so uds reports -// hot-shots' "not installed" error exactly as Node does. -import * as dgram from "node:dgram"; -import * as dns from "node:dns"; -import * as net from "node:net"; -import * as os from "node:os"; +// The hot-shots 8.x shim for installations without the optional unix-dgram +// addon: the uds protocol reports it missing, as hot-shots does under Node. +import { configureUnixDgram } from "./client.ts"; -export type Tags = { [key: string]: string } | string[]; +configureUnixDgram(false); -export interface ClientOptions { - bufferFlushInterval?: number; - bufferHolder?: { buffer: string }; - cacheDns?: boolean; - cacheDnsTtl?: number; - errorHandler?: (err: Error) => void; - globalTags?: Tags; - globalize?: boolean; - host?: string; - isChild?: boolean; - maxBufferSize?: number; - mock?: boolean; - path?: string; - port?: number; - prefix?: string; - protocol?: "tcp" | "udp" | "uds"; - sampleRate?: number; - suffix?: string; - telegraf?: boolean; - useDefaultRoute?: boolean; - tagPrefix?: string; - tagSeparator?: string; - udsGracefulErrorHandling?: boolean; - udsGracefulRestartRateLimit?: number; - closingFlushInterval?: number; -} - -export interface ChildClientOptions { - globalTags?: Tags; - prefix?: string; - suffix?: string; - errorHandler?: (err: Error) => void; -} - -export interface CheckOptions { - /** A Unix timestamp; hot-shots also takes a Date, which this shim's - * option records cannot hold yet. */ - date_happened?: number; - hostname?: string; - message?: string; -} - -export interface DatadogChecks { - OK: 0; - WARNING: 1; - CRITICAL: 2; - UNKNOWN: 3; -} - -export type DatadogChecksValues = 0 | 1 | 2 | 3; - -export interface EventOptions { - aggregation_key?: string; - alert_type?: "info" | "warning" | "success" | "error"; - /** A Unix timestamp; hot-shots also takes a Date, which this shim's - * option records cannot hold yet. */ - date_happened?: number; - hostname?: string; - priority?: "low" | "normal"; - source_type_name?: string; -} - -/** A send's completion: hot-shots passes `null` on success. */ -export type StatsCb = (error: Error | null | undefined, bytes?: any) => void; - -type SampleArg = number | Tags | StatsCb | undefined; -type TagsArg = Tags | StatsCb | undefined; - -const PROTOCOL_TCP = "tcp"; -const PROTOCOL_UDS = "uds"; -const PROTOCOL_UDP = "udp"; -const UDS_DEFAULT_GRACEFUL_RESTART_LIMIT = 1000; -const CACHE_DNS_TTL_DEFAULT = 60000; - -function udsErrorCodes(): number[] { - if (process.platform === "linux") { - return [os.constants.errno.ENOTCONN, os.constants.errno.ECONNREFUSED]; - } - if (process.platform === "darwin") { - return [os.constants.errno.EDESTADDRREQ, os.constants.errno.ECONNRESET]; - } - return []; -} -const UDS_ERROR_CODES = udsErrorCodes(); - -// ── helpers ────────────────────────────────────────────────────────────── - -function sanitizeTags(value: string, telegraf: boolean): string { - const blacklist = telegraf ? /:|\||,/g : /:|\||@|,/g; - return String(value).replace(blacklist, "_"); -} - -function formatTags(tags: Tags, telegraf: boolean): string[] { - if (Array.isArray(tags)) return tags; - const out: string[] = []; - for (const key of Object.keys(tags)) { - out.push(`${sanitizeTags(key, telegraf)}:${sanitizeTags(tags[key]!, telegraf)}`); - } - return out; -} - -function isArrayIndexKey(key: string): boolean { - return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295; -} - -function overrideTags(parent: string[], child: Tags, telegraf: boolean): string[] { - const childKeys: string[] = []; - const childValues = new Map(); - const toAppend: string[] = []; - for (const tag of formatTags(child, telegraf)) { - const idx = tag.indexOf(":"); - if (idx < 1) { - toAppend.push(tag); - } else { - const key = tag.substring(0, idx); - const value = tag.substring(idx + 1); - let values = childValues.get(key); - if (values === undefined) { - values = []; - childValues.set(key, values); - childKeys.push(key); - } - values.push(value); - } - } - const result = parent.filter((tag) => { - const idx = tag.indexOf(":"); - if (idx < 1) return true; - return !childValues.has(tag.substring(0, idx)); - }); - // hot-shots walks an object's keys: array-index keys ascending first, - // then the rest in insertion order. - const indexKeys = childKeys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b)); - const ordered = indexKeys.concat(childKeys.filter((key) => !isArrayIndexKey(key))); - for (const key of ordered) { - for (const value of childValues.get(key)!) result.push(`${key}:${value}`); - } - return result.concat(toAppend); -} - -function formatDate(date: number): number { - return Math.round(date); -} - -function errorWithCode(message: string, code: string | undefined): Error { - const error: NodeJS.ErrnoException = new Error(message); - error.code = code; - return error; -} - -// ── transports ─────────────────────────────────────────────────────────── - -type SendCallback = (err: Error | null, bytes?: number) => void; - -/** The socket a client sends through (hot-shots' transport interface). */ -interface Transport { - createdAt: number; - send(buf: Buffer, callback: SendCallback): void; - close(): void; - onError(listener: (err: Error) => void): void; - offError(listener: (err: Error) => void): void; - onClose(listener: () => void): void; -} - -/** A socket's 'error' listeners, kept by the transport (the typed - * sockets have no removeListener): one socket listener dispatches to a - * snapshot, and an error nobody listens for throws, as an unhandled - * 'error' event does. */ -class ErrorListeners { - private listeners: ((err: Error) => void)[] = []; - add(listener: (err: Error) => void): void { - this.listeners.push(listener); - } - remove(listener: (err: Error) => void): void { - const index = this.listeners.indexOf(listener); - if (index !== -1) this.listeners.splice(index, 1); - } - emit(err: Error): void { - const snapshot = this.listeners.slice(); - if (snapshot.length === 0) throw err; - for (const listener of snapshot) listener(err); - } -} - -function addEol(buf: Buffer): string { - let msg = buf.toString(); - if (msg.length > 0 && msg[msg.length - 1] !== "\n") msg += "\n"; - return msg; -} - -class UdpTransport implements Transport { - createdAt = 0; - private readonly socket: dgram.Socket; - private readonly errors = new ErrorListeners(); - private resolvedAddress: string | undefined = undefined; - private resolvedAt = 0; - private readonly host: string | undefined; - private readonly port: number; - private readonly cacheDns: boolean; - private readonly cacheDnsTtl: number; - constructor(host: string | undefined, port: number, cacheDns: boolean, cacheDnsTtl: number) { - this.host = host; - this.port = port; - this.cacheDns = cacheDns; - this.cacheDnsTtl = cacheDnsTtl; - this.socket = dgram.createSocket("udp4"); - this.socket.unref(); - this.socket.on("error", (err) => this.errors.emit(err)); - } - send(buf: Buffer, callback: SendCallback): void { - if (!this.cacheDns) { - this.socket.send(buf, this.port, this.host || "127.0.0.1", callback); - return; - } - const now = Date.now(); - const cached = this.resolvedAddress; - if (cached === undefined || now - this.resolvedAt > this.cacheDnsTtl) { - dns.lookup(this.host ?? "localhost", (error, address) => { - if (error) { - callback(error); - return; - } - this.resolvedAddress = address; - this.resolvedAt = now; - this.socket.send(buf, this.port, address, callback); - }); - } else { - this.socket.send(buf, this.port, cached, callback); - } - } - close(): void { - this.socket.close(); - } - onError(listener: (err: Error) => void): void { - this.errors.add(listener); - } - offError(listener: (err: Error) => void): void { - this.errors.remove(listener); - } - onClose(listener: () => void): void { - this.socket.on("close", listener); - } -} - -class TcpTransport implements Transport { - createdAt = 0; - private readonly socket: net.Socket; - private readonly errors = new ErrorListeners(); - constructor(host: string | undefined, port: number) { - this.socket = net.connect(port, host ?? "localhost"); - this.socket.setKeepAlive(true); - this.socket.unref(); - this.socket.on("error", (err) => this.errors.emit(err)); - } - send(buf: Buffer, callback: SendCallback): void { - // The typed socket's write takes the data alone; the completion runs - // on the next tick. - this.socket.write(addEol(buf)); - process.nextTick(() => callback(null)); - } - close(): void { - this.socket.destroy(); - } - onError(listener: (err: Error) => void): void { - this.errors.add(listener); - } - offError(listener: (err: Error) => void): void { - this.errors.remove(listener); - } - onClose(listener: () => void): void { - this.socket.on("close", () => listener()); - } -} - -/** hot-shots reaches Unix datagram sockets through the optional - * `unix-dgram` addon; this entry serves programs without it. */ -function createUdsTransport(): Transport { - throw new Error( - "The library `unix_dgram`, needed for the uds protocol to work, is not installed. " + - "You need to pick another protocol to use hot-shots. " + - "See the hot-shots README for additional details.", - ); -} - -function createTransport(client: StatsD): Transport | null { - let transport: Transport | null = null; - const protocol = client.protocol; - try { - if (protocol === PROTOCOL_TCP) { - transport = new TcpTransport(client.host, client.port); - } else if (protocol === PROTOCOL_UDS) { - transport = createUdsTransport(); - } else if (protocol === PROTOCOL_UDP) { - transport = new UdpTransport(client.host, client.port, client.cacheDns, client.cacheDnsTtl); - } else { - throw new Error(`Unsupported protocol '${protocol}'`); - } - transport.createdAt = Date.now(); - } catch (e) { - const handler = client.errorHandler; - if (handler) { - handler(e as Error); - } else { - console.error(e); - } - } - return transport; -} - -// ── the client ─────────────────────────────────────────────────────────── - -class BufferHolder { - buffer = ""; -} - -/** What a child client inherits from its parent. */ -class ChildInit { - socket: Transport | null; - bufferHolder: BufferHolder; - dnsError: Error | undefined; - constructor(socket: Transport | null, bufferHolder: BufferHolder, dnsError: Error | undefined) { - this.socket = socket; - this.bufferHolder = bufferHolder; - this.dnsError = dnsError; - } -} - -export class StatsD { - protocol: string; - cacheDns: boolean; - cacheDnsTtl: number; - host: string | undefined; - port: number; - path: string | undefined; - prefix: string; - suffix: string; - tagPrefix: string; - tagSeparator: string; - mock: boolean; - globalTags: string[]; - telegraf: boolean; - maxBufferSize: number; - sampleRate: number; - bufferFlushInterval: number; - errorHandler: ((err: Error) => void) | undefined; - udsGracefulErrorHandling: boolean; - udsGracefulRestartRateLimit: number; - isChild: boolean; - closingFlushInterval: number; - mockBuffer: string[] | undefined = undefined; - messagesInFlight = 0; - CHECKS: DatadogChecks = { OK: 0, WARNING: 1, CRITICAL: 2, UNKNOWN: 3 }; - private bufferHolder: BufferHolder; - private transport: Transport | null = null; - private dnsError: Error | undefined = undefined; - private stopFlushInterval: (() => void) | undefined = undefined; - - constructor(options?: ClientOptions, child?: ChildInit) { - const o: ClientOptions = options ?? {}; - const protocol = o.protocol ? o.protocol.toLowerCase() : ""; - this.protocol = protocol ? protocol : PROTOCOL_UDP; - this.cacheDns = o.cacheDns === true; - this.cacheDnsTtl = o.cacheDnsTtl || CACHE_DNS_TTL_DEFAULT; - this.host = o.host || process.env["DD_AGENT_HOST"]; - this.port = o.port || parseInt(process.env["DD_DOGSTATSD_PORT"] ?? "", 10) || 8125; - this.path = o.path; - this.prefix = o.prefix || ""; - this.suffix = o.suffix || ""; - this.tagPrefix = o.tagPrefix || "#"; - this.tagSeparator = o.tagSeparator || ","; - this.mock = o.mock === true; - this.telegraf = o.telegraf || false; - const globalTags = o.globalTags; - this.globalTags = globalTags !== undefined ? formatTags(globalTags, this.telegraf) : []; - const entityId = process.env["DD_ENTITY_ID"]; - if (entityId) { - this.globalTags = this.globalTags.filter( - (item) => item.indexOf("dd.internal.entity_id:") !== 0, - ); - this.globalTags.push("dd.internal.entity_id:".concat(sanitizeTags(entityId, false))); - } - this.maxBufferSize = o.maxBufferSize || 0; - this.sampleRate = o.sampleRate || 1; - this.bufferFlushInterval = o.bufferFlushInterval || 1000; - this.bufferHolder = child !== undefined ? child.bufferHolder : new BufferHolder(); - this.errorHandler = o.errorHandler; - const graceful = o.udsGracefulErrorHandling; - this.udsGracefulErrorHandling = graceful === undefined ? true : graceful; - this.udsGracefulRestartRateLimit = - o.udsGracefulRestartRateLimit || UDS_DEFAULT_GRACEFUL_RESTART_LIMIT; - this.isChild = child !== undefined; - this.closingFlushInterval = o.closingFlushInterval || 50; - if (this.mock) this.mockBuffer = []; - if (child === undefined && this.maxBufferSize > 0) { - const handle = setInterval(() => this.onBufferFlushInterval(), this.bufferFlushInterval); - handle.unref(); - this.stopFlushInterval = () => clearInterval(handle); - } - if (child !== undefined) { - if (child.dnsError) this.dnsError = child.dnsError; - this.transport = child.socket; - } - if (!this.transport) this.transport = createTransport(this); - const handler = o.errorHandler; - const transport = this.transport; - if (transport !== null && child === undefined && handler) transport.onError(handler); - if (transport !== null && o.protocol === PROTOCOL_UDS) this.maybeAddUDSErrorHandler(); - } - - private maybeAddUDSErrorHandler(): void { - if (this.isChild || !this.udsGracefulErrorHandling) return; - const transport = this.transport; - if (transport !== null) transport.onError((err) => this.udsErrorHandler(err)); - } - - private udsErrorHandler(err: Error): void { - const code = Number((err as NodeJS.ErrnoException).code); - if (!UDS_ERROR_CODES.includes(-code)) return; - const socket = this.transport; - if (socket === null) return; - if (!socket.createdAt || Date.now() - socket.createdAt < this.udsGracefulRestartRateLimit) { - return; - } - const handler = this.errorHandler; - if (handler) socket.offError(handler); - const newSocket = createTransport(this); - if (newSocket === null) { - const errorMessage = "Could not replace UDS connection with new socket"; - if (handler) { - handler(new Error(errorMessage)); - } else { - console.error(errorMessage); - } - return; - } - socket.close(); - this.transport = newSocket; - this.maybeAddUDSErrorHandler(); - if (handler) { - newSocket.onError(handler); - } else { - newSocket.onError((error) => console.error(`hot-shots UDS error: ${String(error)}`)); - } - } - - // ── stats functions ── - - timing(stat: string | string[], time: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; - timing(stat: string | string[], time: number, callback?: StatsCb): void; - timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; - timing( - stat: string | string[], - time: number, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - this.sendAll(stat, time, "ms", sampleRate, tags, callback); - } - - increment(stat: string, tags?: Tags): void; - increment(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; - increment(stat: string | string[], value: number, callback?: StatsCb): void; - increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; - increment( - stat: string | string[], - value?: number | Tags, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - // A non-number second argument of a two-argument call is the tags. - let amount = 1; - let tagArg: TagsArg = tags; - if (typeof value === "number") { - amount = value; - } else if (sampleRate === undefined && tags === undefined && callback === undefined) { - tagArg = value; - } - this.sendAll(stat, amount, "c", sampleRate, tagArg, callback); - } - - decrement(stat: string, tags?: Tags): void; - decrement(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; - decrement(stat: string | string[], value: number, callback?: StatsCb): void; - decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; - decrement( - stat: string | string[], - value?: number | Tags, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - let amount = 1; - let tagArg: TagsArg = tags; - if (typeof value === "number") { - amount = value; - } else if (sampleRate === undefined && tags === undefined && callback === undefined) { - tagArg = value; - } - this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); - } - - histogram(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; - histogram(stat: string | string[], value: number, callback?: StatsCb): void; - histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; - histogram( - stat: string | string[], - value: number, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - this.sendAll(stat, value, "h", sampleRate, tags, callback); - } - - distribution(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; - distribution(stat: string | string[], value: number, callback?: StatsCb): void; - distribution(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; - distribution( - stat: string | string[], - value: number, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - this.sendAll(stat, value, "d", sampleRate, tags, callback); - } - - gauge(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; - gauge(stat: string | string[], value: number, callback?: StatsCb): void; - gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; - gauge( - stat: string | string[], - value: number, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - this.sendAll(stat, value, "g", sampleRate, tags, callback); - } - - set(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; - set(stat: string | string[], value: number | string, callback?: StatsCb): void; - set(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; - set( - stat: string | string[], - value: number | string, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - this.sendAll(stat, value, "s", sampleRate, tags, callback); - } - - unique(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; - unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; - unique(stat: string | string[], value: number | string, callback?: StatsCb): void; - unique(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; - unique( - stat: string | string[], - value: number | string, - sampleRate?: SampleArg, - tags?: TagsArg, - callback?: StatsCb, - ): void { - this.sendAll(stat, value, "s", sampleRate, tags, callback); - } - - check( - name: string, - status: DatadogChecksValues, - options?: CheckOptions, - tags?: Tags | StatsCb, - callback?: StatsCb, - ): void { - if (this.telegraf) { - const err = new Error("Not supported by Telegraf / InfluxDB"); - if (callback) return callback(err); - const handler = this.errorHandler; - if (handler) return handler(err); - throw err; - } - const check: string[] = ["_sc", this.prefix + name + this.suffix, String(status)]; - const metadata: CheckOptions = options ?? {}; - const happened = metadata.date_happened; - if (happened) { - const timestamp = formatDate(happened); - if (timestamp) check.push(`d:${timestamp}`); - } - if (metadata.hostname) check.push(`h:${metadata.hostname}`); - let mergedTags = this.globalTags; - if (tags !== undefined && typeof tags !== "function") { - mergedTags = overrideTags(mergedTags, tags, this.telegraf); - } - if (mergedTags.length > 0) check.push(`#${mergedTags.join(",")}`); - if (metadata.message) check.push(`m:${metadata.message}`); - let cb = callback; - if (typeof tags === "function" && callback === undefined) cb = tags; - this.sendRaw(check.join("|"), cb); - } - - event( - title: string, - text?: string, - options?: EventOptions, - tags?: Tags | StatsCb, - callback?: StatsCb, - ): void { - if (this.telegraf) { - const err = new Error("Not supported by Telegraf / InfluxDB"); - if (callback) return callback(err); - const handler = this.errorHandler; - if (handler) return handler(err); - throw err; - } - const msgTitle = String(title ? title : ""); - let msgText = String(text ? text : msgTitle); - msgText = msgText.replace(/\n/g, "\\n"); - let message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; - if (options) { - const happened = options.date_happened; - if (happened) { - const timestamp = formatDate(happened); - if (timestamp) message += `|d:${timestamp}`; - } - if (options.hostname) message += `|h:${options.hostname}`; - if (options.aggregation_key) message += `|k:${options.aggregation_key}`; - if (options.priority) message += `|p:${options.priority}`; - if (options.source_type_name) message += `|s:${options.source_type_name}`; - if (options.alert_type) message += `|t:${options.alert_type}`; - } - let cb = callback; - let tagList: Tags | undefined = undefined; - if (typeof tags === "function") { - if (callback === undefined) cb = tags; - } else { - tagList = tags; - } - this.send(message, tagList, cb); - } - - // ── sending ── - - private sendAll( - stat: string | string[], - value: number | string, - type: string, - sampleRateArg: SampleArg, - tagsArg: TagsArg, - callbackArg: StatsCb | undefined, - ): void { - let sampleRate: number | undefined = undefined; - let tags: Tags | undefined = undefined; - let callback: StatsCb | undefined = callbackArg; - if (typeof sampleRateArg === "number") { - sampleRate = sampleRateArg; - if (typeof tagsArg === "function") { - callback = tagsArg; - } else { - tags = tagsArg; - } - } else if (sampleRateArg !== undefined) { - // A non-number sample rate shifts into the tags, and the tags - // argument into the callback. - if (typeof sampleRateArg === "function") { - callback = sampleRateArg; - } else { - tags = sampleRateArg; - callback = typeof tagsArg === "function" ? tagsArg : undefined; - } - } else if (typeof tagsArg === "function") { - callback = tagsArg; - } else { - tags = tagsArg; - } - - if (!Array.isArray(stat)) { - this.sendStat(stat, value, type, sampleRate, tags, callback); - return; - } - let completed = 0; - let calledback = false; - let sentBytes = 0; - const count = stat.length; - const onSend: StatsCb = (error, bytes) => { - completed += 1; - if (calledback) return; - if (error) { - if (callback !== undefined) { - calledback = true; - callback(error); - } else { - const handler = this.errorHandler; - if (handler) { - calledback = true; - handler(error); - } - } - return; - } - if (bytes) sentBytes += bytes as number; - if (completed === count && callback !== undefined) callback(null, sentBytes); - }; - for (const item of stat) this.sendStat(item, value, type, sampleRate, tags, onSend); - } - - private sendStat( - stat: string, - value: number | string, - type: string, - sampleRateArg: number | undefined, - tags: Tags | undefined, - callback: StatsCb | undefined, - ): void { - let message = `${this.prefix + stat + this.suffix}:${value}|${type}`; - const sampleRate = sampleRateArg || this.sampleRate; - if (sampleRate && sampleRate < 1) { - if (Math.random() < sampleRate) { - message += `|@${sampleRate}`; - } else { - if (callback) callback(undefined); - return; - } - } - this.send(message, tags, callback); - } - - private send(message: string, tags: Tags | undefined, callback: StatsCb | undefined): void { - let mergedTags = this.globalTags; - if (tags !== undefined) mergedTags = overrideTags(mergedTags, tags, this.telegraf); - if (mergedTags.length > 0) { - if (this.telegraf) { - const parts = message.split(":"); - message = `${parts[0]!},${mergedTags.join(",").replace(/:/g, "=")}:${parts.slice(1).join(":")}`; - } else { - message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`; - } - } - this.sendRaw(message, callback); - } - - /** hot-shots' `_send`: the message with its tags. */ - private sendRaw(message: string, callback: StatsCb | undefined): void { - const dnsError = this.dnsError; - if (dnsError) { - if (callback) return callback(dnsError); - const handler = this.errorHandler; - if (handler) return handler(dnsError); - throw dnsError; - } - if (!this.mock) { - if (this.maxBufferSize === 0) { - this.sendMessage(message, callback); - } else { - this.enqueue(message, callback); - } - } else { - const buffer = this.mockBuffer; - if (buffer !== undefined) buffer.push(message); - if (callback) callback(null, 0); - } - } - - private enqueue(message: string, callback: StatsCb | undefined): void { - message += "\n"; - if (this.bufferHolder.buffer.length + message.length > this.maxBufferSize) { - this.flushQueue(callback); - this.bufferHolder.buffer += message; - } else { - this.bufferHolder.buffer += message; - if (callback) callback(null); - } - } - - private flushQueue(callback?: StatsCb): void { - this.sendMessage(this.bufferHolder.buffer, callback); - this.bufferHolder.buffer = ""; - } - - private sendMessage(message: string, callback: StatsCb | undefined): void { - if (message === "") { - if (callback) callback(null); - return; - } - const socketWasMissing = this.transport === null; - if (socketWasMissing && this.protocol === PROTOCOL_UDS) { - this.transport = createTransport(this); - if (this.transport !== null) this.maybeAddUDSErrorHandler(); - } - const socket = this.transport; - if (socketWasMissing || socket === null) { - const error = new Error("Socket not created properly. Check previous errors for details."); - if (callback) return callback(error); - const handler = this.errorHandler; - if (handler) return handler(error); - console.error(String(error)); - return; - } - const handleCallback = (err: Error | null, bytes?: number): void => { - this.messagesInFlight--; - let errFormatted: Error | null = null; - if (err) { - errFormatted = errorWithCode( - `Error sending hot-shots message: ${String(err)}`, - (err as NodeJS.ErrnoException).code, - ); - if (this.protocol === PROTOCOL_UDS && (callback || this.errorHandler)) { - this.udsErrorHandler(err); - } - } - if (callback) { - callback(errFormatted, bytes); - } else if (errFormatted) { - const handler = this.errorHandler; - if (handler) { - handler(errFormatted); - } else { - console.error(String(errFormatted)); - } - } - }; - try { - this.messagesInFlight++; - socket.send(Buffer.from(message), handleCallback); - } catch (err) { - handleCallback(err as Error); - } - } - - private onBufferFlushInterval(): void { - this.flushQueue(); - } - - close(callback?: (error?: Error) => void): void { - const stop = this.stopFlushInterval; - if (stop !== undefined) stop(); - this.flushQueue((err) => { - if (err) { - if (callback) return callback(err); - console.error(err); - return; - } - let intervalAttempts = 0; - const waitForMessages = setInterval(() => { - intervalAttempts++; - if (intervalAttempts > 10) { - console.log("hot-shots could not clear out messages in flight but closing anyways"); - this.messagesInFlight = 0; - } - if (this.messagesInFlight <= 0) { - clearInterval(waitForMessages); - this.closeSocket(callback); - } - }, this.closingFlushInterval); - }); - } - - private closeSocket(callback: ((error?: Error) => void) | undefined): void { - let handledError = false; - const handleErr = (err: unknown): void => { - const errMessage = `Error closing hot-shots socket: ${String(err)}`; - if (handledError) { - console.error(errMessage); - } else { - handledError = true; - if (callback) { - callback(new Error(errMessage)); - } else { - const handler = this.errorHandler; - if (handler) { - handler(new Error(errMessage)); - } else { - console.error(errMessage); - } - } - } - }; - const socket = this.transport; - if (socket === null) { - handleErr(new TypeError("Cannot read properties of null (reading 'removeListener')")); - return; - } - const handler = this.errorHandler; - if (handler) socket.offError(handler); - socket.onError(handleErr); - if (callback) { - socket.onClose(() => { - if (!handledError) callback(undefined); - }); - } - try { - socket.close(); - } catch (err) { - handleErr(err); - } - } - - childClient(options?: ChildClientOptions): StatsD { - const o: ChildClientOptions = options ?? {}; - const childTags = o.globalTags; - return new StatsD( - { - isChild: true, - errorHandler: o.errorHandler || this.errorHandler, - host: this.host, - port: this.port, - tagPrefix: this.tagPrefix, - tagSeparator: this.tagSeparator, - prefix: (o.prefix || "") + this.prefix, - suffix: this.suffix + (o.suffix || ""), - globalize: false, - mock: this.mock, - globalTags: - childTags !== undefined - ? overrideTags(this.globalTags, childTags, this.telegraf) - : this.globalTags, - maxBufferSize: this.maxBufferSize, - bufferFlushInterval: this.bufferFlushInterval, - telegraf: this.telegraf, - protocol: this.protocol === "tcp" ? "tcp" : this.protocol === "uds" ? "uds" : "udp", - closingFlushInterval: this.closingFlushInterval, - }, - new ChildInit(this.transport, this.bufferHolder, this.dnsError), - ); - } -} - -export default StatsD; +export * from "./client.ts"; +export { default } from "./client.ts"; diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 184fdb99e..6e10e9c62 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -105,6 +105,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L "dgram.sendStr": "scr_dgram_send_str", "dgram.sendBytes": "scr_dgram_send_bytes", "dgram.sendChk": "scr_dgram_send_chk", + "dgram.unixSocket": "scr_dgram_unix_socket", + "dgram.unixConnect": "scr_dgram_unix_connect", + "dgram.unixSend": "scr_dgram_unix_send", + "dgram.unixClose": "scr_dgram_unix_close", "dgram.ref": "scr_dgram_ref", "dgram.unref": "scr_dgram_unref", "dgram.setBroadcast": "scr_dgram_set_broadcast", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 53971c7b0..3c04924c0 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -45,6 +45,7 @@ import { errorToStringCall } from "./error-methods.js"; import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js"; import { BYTES_ELEMENT_NAME, + BYTES_U8, BIGINT_T, BOOL, CAUGHT, @@ -53,6 +54,7 @@ import { F64, type IrExpr, type IrFunction, + type IrLibFn, type IrLocal, type IrParam, type IrStmt, @@ -76,6 +78,7 @@ import { } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; +import { isPackageShimFile, isShimIntrinsicName } from "../package-shims.js"; import { lowerJsonRequireCall } from "./json-require.js"; import { lowerAssertInstanceCall } from "./assert-instances.js"; import { @@ -292,6 +295,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // A package shim's runtime intrinsic (a bodiless `declare function` + // named scriptcUnixDgram* in a shipped shim): a direct runtime call. + if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { + const intrinsic = lowerShimIntrinsicCall(lowerer, expr, expr.expression, loc); + if (intrinsic) return intrinsic; + } // Inline require() of a program module (cjs-inline-require.ts): a // member call over its export table resolves like a binding's member // call after the module's %init at this position; the require itself @@ -10958,3 +10967,37 @@ 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)`, ); } + +/** The runtime calls package shims declare as bodiless functions. */ +const SHIM_INTRINSICS: ReadonlyMap = new Map< + string, + { fn: IrLibFn; args: IrType[]; ret: IrType } +>([ + ["scriptcUnixDgramSocket", { fn: "dgram.unixSocket", args: [], ret: F64 }], + ["scriptcUnixDgramConnect", { fn: "dgram.unixConnect", args: [F64, STRING], ret: F64 }], + ["scriptcUnixDgramSend", { fn: "dgram.unixSend", args: [F64, BYTES_U8], ret: F64 }], + ["scriptcUnixDgramClose", { fn: "dgram.unixClose", args: [F64], ret: F64 }], +]); + +function lowerShimIntrinsicCall( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: ts.Identifier, + loc: SrcLoc, +): IrExpr | null { + const intrinsic = SHIM_INTRINSICS.get(callee.text); + if (intrinsic === undefined) return null; + const decl = lowerer.checker.getSymbolAtLocation(callee)?.valueDeclaration; + if ( + decl === undefined || + !ts.isFunctionDeclaration(decl) || + decl.body || + !isPackageShimFile(ts.getSourceFileOfNode(decl).fileName) || + expr.arguments.nodes.length !== intrinsic.args.length + ) + return null; + const args = expr.arguments.nodes.map((arg, i) => + lowerer.lowerExprExpecting(arg, intrinsic.args[i]!), + ); + return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 19123dae3..ea844ab1e 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -38,7 +38,7 @@ * calls). */ import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; -import { packageShimFor } from "../package-shims.js"; +import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; @@ -654,6 +654,12 @@ export function ambientUndefinedFnSymbolOf( const sym = lowerer.resolveValueSymbol(ident); if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null; if (lowerer.isStdlibSymbol(sym)) return null; + // A package shim's runtime intrinsic (lower-calls.ts) is defined. + if ( + isShimIntrinsicName(ident.text) && + sym.declarations.some((d) => isPackageShimFile(ts.getSourceFileOfNode(d).fileName)) + ) + return null; let sawFn = false; for (const d of sym.declarations) { if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index f9cf1deb6..3af7ebf59 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -65,9 +65,14 @@ interface ImplementationShim { /** The package's declaration entry relative to its directory, when it * is not `index.d.ts`. */ typesFile?: string; + /** An alternative entry for this installation (relative to the shim + * root), or undefined for the version's own entry. */ + variant?: (pkgDir: string) => string | undefined; + /** Every variant entry, for the guard test that compiles each one. */ + variants?: readonly string[]; } -const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ [ "fs-extra", { @@ -100,6 +105,13 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ mainFile: "index.js", typesFile: "types.d.ts", versions: new Map([[8, "hot-shots/v8.ts"]]), + // hot-shots' uds protocol loads the optional unix-dgram addon: where + // it is installed, the entry with the native Unix datagram transport. + variant: (pkgDir: string) => + installedPackageDir(join(pkgDir, "index.js"), "unix-dgram") === null + ? undefined + : "hot-shots/v8-unix-dgram.ts", + variants: ["hot-shots/v8-unix-dgram.ts"], }, ], [ @@ -164,8 +176,10 @@ export function throughBundledShimForwarder(resolved: string): string { * the guard test that compiles each one. */ export function packageShimEntries(): string[] { const entries = new Set(); - for (const shim of IMPLEMENTATION_SHIMS.values()) + for (const shim of IMPLEMENTATION_SHIMS.values()) { for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry)); + for (const entry of shim.variants ?? []) entries.add(join(packageShimRoot(), entry)); + } return [...entries].sort(); } @@ -263,7 +277,8 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st if (shim === undefined) return null; const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); const entry = shim.versions.get(major); - return entry === undefined ? null : join(packageShimRoot(), entry); + if (entry === undefined) return null; + return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry); } /** The shim module `specifier` resolves to from `fromFile`, or null. Only @@ -355,6 +370,12 @@ export function isPackageShimFile(fileName: string): boolean { return fileName.startsWith(root.endsWith("/") ? root : `${root}/`); } +/** 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"); +} + /** Resets per-compile caches. */ export function clearPackageShimCaches(): void { installedCache.clear(); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..29385db37 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1528,6 +1528,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ }, "dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID }, "dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID }, + /** Connected AF_UNIX datagram sockets for the hot-shots package shim + * (the unix-dgram addon's surface): fd or result >= 0, -errno on + * failure; send answers 1 when the socket would block. */ + "dgram.unixSocket": { argTypes: [], result: F64 }, + "dgram.unixConnect": { argTypes: [F64, STRING], result: F64 }, + "dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 }, + "dgram.unixClose": { argTypes: [F64], result: F64 }, /** The send argument-validation ladder over dyn arguments (Node's * signature shuffle: slice bounds, list/type contracts, port/address * validation, connected-state errors), then the send: lists, connected diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index bb14d063e..8d1c7b058 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -2158,3 +2158,73 @@ void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a * it removes a 'message' listener. Defined last so the functions above * keep their emission order. */ void scr_dgram_rearm(ScrDgramSocket *s) { scr_dgram_update_read(s); } + +/* ── Unix datagram sockets for the hot-shots package shim ────────────── + * The unix-dgram addon's surface, which hot-shots' uds transport uses: a + * non-blocking AF_UNIX datagram socket connected to a path. Each call + * answers its result (fd, 0) or a negative errno, as the addon's binding + * does; send answers 1 when the socket would block (the addon's + * "congestion"). Defined after the functions above so their emission + * order is unchanged. */ +#ifndef _WIN32 +#include +#endif + +double scr_dgram_unix_socket(void) { +#ifdef _WIN32 + return -(double)ENOSYS; +#else + int fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (fd < 0) return -(double)errno; + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK); + (void)fcntl(fd, F_SETFD, FD_CLOEXEC); + return (double)fd; +#endif +} + +double scr_dgram_unix_connect(double fd, ScrStr *path) { +#ifdef _WIN32 + (void)fd; + (void)path; + return -(double)ENOSYS; +#else + struct sockaddr_un addr; + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + char *owned; + const char *bytes = scr_str_utf8(path, &owned); + size_t n = strlen(bytes); + if (n > sizeof addr.sun_path - 1) n = sizeof addr.sun_path - 1; + memcpy(addr.sun_path, bytes, n); + scr_str_utf8_end(owned); + if (connect((int)fd, (struct sockaddr *)&addr, sizeof addr) != 0) return -(double)errno; + return 0; +#endif +} + +double scr_dgram_unix_send(double fd, ScrBytes *data) { +#ifdef _WIN32 + (void)fd; + (void)data; + return -(double)ENOSYS; +#else + SCR_BYTES_SNAPSHOT(data); + ssize_t sent; + do { + sent = send((int)fd, data->data, data->len, 0); + } while (sent < 0 && errno == EINTR); + if (sent >= 0) return 0; + if (errno == EAGAIN || errno == EWOULDBLOCK) return 1; + return -(double)errno; +#endif +} + +double scr_dgram_unix_close(double fd) { +#ifdef _WIN32 + (void)fd; + return -(double)ENOSYS; +#else + return close((int)fd) != 0 ? -(double)errno : 0; +#endif +} From eef238eb9473ac937c5cf9ed1d1af73cc8930f8c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 21:59:20 -0700 Subject: [PATCH 016/191] Pin chalk builder values and buffered hot-shots against Node A callable class instance in a narrower function slot crosses the checked function boundary. New npm cases: chalk5-builder, hot-shots-buffered. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 16 + .../compiler/test/package-shim-guard.test.ts | 3 + .../fixtures/npm/cases/chalk5-builder/main.ts | 45 +++ .../chalk5-builder/node_modules/chalk/license | 9 + .../node_modules/chalk/package.json | 83 +++++ .../node_modules/chalk/source/index.d.ts | 325 ++++++++++++++++++ .../node_modules/chalk/source/index.js | 225 ++++++++++++ .../node_modules/chalk/source/utilities.js | 33 ++ .../source/vendor/ansi-styles/index.d.ts | 236 +++++++++++++ .../chalk/source/vendor/ansi-styles/index.js | 223 ++++++++++++ .../source/vendor/supports-color/browser.d.ts | 1 + .../source/vendor/supports-color/browser.js | 34 ++ .../source/vendor/supports-color/index.d.ts | 55 +++ .../source/vendor/supports-color/index.js | 190 ++++++++++ .../npm/cases/chalk5-builder/package.json | 1 + .../npm/cases/hot-shots-buffered/main.ts | 80 +++++ .../npm/cases/hot-shots-buffered/package.json | 1 + 17 files changed, 1560 insertions(+) create mode 100644 tests/fixtures/npm/cases/chalk5-builder/main.ts create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js create mode 100644 tests/fixtures/npm/cases/chalk5-builder/package.json create mode 100644 tests/fixtures/npm/cases/hot-shots-buffered/main.ts create mode 100644 tests/fixtures/npm/cases/hot-shots-buffered/package.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b1867be32..ac1a778cf 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -230,6 +230,7 @@ import { arrayOf, BOOL, canAdaptDynFuncTo, + canBoxFuncIntoDyn, canDynCheckTo, canConvertToDyn, DYN, @@ -9642,6 +9643,21 @@ export class Lowerer { if (expected.kind === "func" && (expr.type.kind === "record" || expr.type.kind === "object")) { const un = this.hybridCallUnwrap(expr); if (un !== expr && typeEquals(un.type, expected)) return un; + // A callable class instance (the chalk shim's variadic builder) in a + // narrower function slot: its call closure crosses the checked + // function boundary, which adapts the arity. + if ( + un !== expr && + expr.type.kind === "object" && + un.type.kind === "func" && + canBoxFuncIntoDyn( + un.type, + (id) => this.shapes.get(id), + (id) => this.unions.get(id), + ) + ) { + return this.coerceToExpected({ kind: "dynFrom", value: un, type: DYN, loc: un.loc }, expected); + } } // A zero-param function whose RETURN is a wider record array than the // slot's (`getRoutes: () => cachedRoutes` against `() => Narrow[]`): diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts index ff01acbb8..a8620c1ce 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -49,7 +49,10 @@ describe("package shims compile without refusals", () => { const entries = packageShimEntries(); test("the registry lists its shim modules", () => { expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ + "chalk/v5.ts", "fs-extra/index.ts", + "hot-shots/v8-unix-dgram.ts", + "hot-shots/v8.ts", "undici/v5.ts", "undici/v7.ts", ]); diff --git a/tests/fixtures/npm/cases/chalk5-builder/main.ts b/tests/fixtures/npm/cases/chalk5-builder/main.ts new file mode 100644 index 000000000..8f2623717 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/main.ts @@ -0,0 +1,45 @@ +// chalk 5 builders used as values (the package compiles from scriptc's +// built-in shim): builders in function slots and arrays, styles picked at +// run time, visible at level 0, chalkStderr, and the root's own call. +import chalk, { Chalk, chalkStderr, type ChalkInstance, type ForegroundColorName } from "chalk"; + +const show = (label: string, value: string): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +const c = new Chalk({ level: 1 }); +const paint: (text: string) => string = c.green; +show("slot", paint("go")); +const styles: ChalkInstance[] = [c.red, c.bold, c.underline.blue]; +show("array", styles.map((style) => style("x")).join("|")); +const byName = (name: ForegroundColorName): ChalkInstance => { + switch (name) { + case "red": + return c.red; + case "green": + return c.green; + default: + return c.white; + } +}; +show("picked", byName("red")("r") + byName("green")("g") + byName("cyan")("w")); +let chain: ChalkInstance = c; +for (const step of ["bold", "italic", "yellow"]) { + chain = step === "bold" ? chain.bold : step === "italic" ? chain.italic : chain.yellow; +} +show("built", chain("chain")); +show("cached", c.red === c.red ? "same" : "different"); +show("nested close", c.red("a" + c.blue("b") + "c" + c.red("d") + "e")); +show("bg nested", c.bgRed("a" + c.bgBlue("b") + "c")); + +const off = new Chalk({ level: 0 }); +show("visible off", off.visible("v") + "|" + off.red.visible("v") + "|" + off.red("plain")); +show("root call", c("a", 1, true) + "|" + c() + "|" + c(undefined) + "|" + c(null, "n")); +show("builder args", c.red(undefined) + "|" + c.red(null) + "|" + c.red(1, 2) + "|" + c.red()); +show("multiline", c.cyan("one\r\ntwo\nthree")); +show("stderr level", String(chalkStderr.level >= 0)); +show("rgb levels", [1, 2, 3].map((level) => new Chalk({ level: level as 1 | 2 | 3 }).rgb(255, 136, 0)("o")).join("|")); +show("hex bg", new Chalk({ level: 2 }).bgHex("#abcdef")("h") + new Chalk({ level: 1 }).bgHex("123")("i")); +c.level = 0; +show("after level 0", paint("go") + "|" + c.red("r")); +show("default red", chalk.level >= 0 ? "ok" : "bad"); diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license new file mode 100644 index 000000000..fa7ceba3e --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +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/chalk5-builder/node_modules/chalk/package.json b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json new file mode 100644 index 000000000..c9e0dc52b --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/package.json @@ -0,0 +1,83 @@ +{ + "name": "chalk", + "version": "5.6.2", + "description": "Terminal string styling done right", + "license": "MIT", + "repository": "chalk/chalk", + "funding": "https://github.com/chalk/chalk?sponsor=1", + "type": "module", + "main": "./source/index.js", + "exports": "./source/index.js", + "imports": { + "#ansi-styles": "./source/vendor/ansi-styles/index.js", + "#supports-color": { + "node": "./source/vendor/supports-color/index.js", + "default": "./source/vendor/supports-color/browser.js" + } + }, + "types": "./source/index.d.ts", + "sideEffects": false, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "scripts": { + "test": "xo && c8 ava && tsd", + "bench": "matcha benchmark.js" + }, + "files": [ + "source", + "!source/index.test-d.ts" + ], + "keywords": [ + "color", + "colour", + "colors", + "terminal", + "console", + "cli", + "string", + "ansi", + "style", + "styles", + "tty", + "formatting", + "rgb", + "256", + "shell", + "xterm", + "log", + "logging", + "command-line", + "text" + ], + "devDependencies": { + "@types/node": "^16.11.10", + "ava": "^3.15.0", + "c8": "^7.10.0", + "color-convert": "^2.0.1", + "execa": "^6.0.0", + "log-update": "^5.0.0", + "matcha": "^0.7.0", + "tsd": "^0.19.0", + "xo": "^0.57.0", + "yoctodelay": "^2.0.0" + }, + "xo": { + "rules": { + "unicorn/prefer-string-slice": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/consistent-type-exports": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "unicorn/expiring-todo-comments": "off" + } + }, + "c8": { + "reporter": [ + "text", + "lcov" + ], + "exclude": [ + "source/vendor" + ] + } +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..8295d9252 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.d.ts @@ -0,0 +1,325 @@ +// TODO: Make it this when TS suports that. +// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles'; +// import {ColorInfo, ColorSupportLevel} from '#supports-color'; +import { + ModifierName, + ForegroundColorName, + BackgroundColorName, + ColorName, +} from './vendor/ansi-styles/index.js'; +import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js'; + +export interface Options { + /** + Specify the color support for Chalk. + + By default, color support is automatically detected based on the environment. + + Levels: + - `0` - All colors disabled. + - `1` - Basic 16 colors support. + - `2` - ANSI 256 colors support. + - `3` - Truecolor 16 million colors support. + */ + readonly level?: ColorSupportLevel; +} + +/** +Return a new Chalk instance. +*/ +export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention + +export interface ChalkInstance { + (...text: unknown[]): string; + + /** + The color support for Chalk. + + By default, color support is automatically detected based on the environment. + + Levels: + - `0` - All colors disabled. + - `1` - Basic 16 colors support. + - `2` - ANSI 256 colors support. + - `3` - Truecolor 16 million colors support. + */ + level: ColorSupportLevel; + + /** + Use RGB values to set text color. + + @example + ``` + import chalk from 'chalk'; + + chalk.rgb(222, 173, 237); + ``` + */ + rgb: (red: number, green: number, blue: number) => this; + + /** + Use HEX value to set text color. + + @param color - Hexadecimal value representing the desired color. + + @example + ``` + import chalk from 'chalk'; + + chalk.hex('#DEADED'); + ``` + */ + hex: (color: string) => this; + + /** + Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color. + + @example + ``` + import chalk from 'chalk'; + + chalk.ansi256(201); + ``` + */ + ansi256: (index: number) => this; + + /** + Use RGB values to set background color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgRgb(222, 173, 237); + ``` + */ + bgRgb: (red: number, green: number, blue: number) => this; + + /** + Use HEX value to set background color. + + @param color - Hexadecimal value representing the desired color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgHex('#DEADED'); + ``` + */ + bgHex: (color: string) => this; + + /** + Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color. + + @example + ``` + import chalk from 'chalk'; + + chalk.bgAnsi256(201); + ``` + */ + bgAnsi256: (index: number) => this; + + /** + Modifier: Reset the current style. + */ + readonly reset: this; + + /** + Modifier: Make the text bold. + */ + readonly bold: this; + + /** + Modifier: Make the text have lower opacity. + */ + readonly dim: this; + + /** + Modifier: Make the text italic. *(Not widely supported)* + */ + readonly italic: this; + + /** + Modifier: Put a horizontal line below the text. *(Not widely supported)* + */ + readonly underline: this; + + /** + Modifier: Put a horizontal line above the text. *(Not widely supported)* + */ + readonly overline: this; + + /** + Modifier: Invert background and foreground colors. + */ + readonly inverse: this; + + /** + Modifier: Print the text but make it invisible. + */ + readonly hidden: this; + + /** + Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)* + */ + readonly strikethrough: this; + + /** + Modifier: Print the text only when Chalk has a color level above zero. + + Can be useful for things that are purely cosmetic. + */ + readonly visible: this; + + readonly black: this; + readonly red: this; + readonly green: this; + readonly yellow: this; + readonly blue: this; + readonly magenta: this; + readonly cyan: this; + readonly white: this; + + /* + Alias for `blackBright`. + */ + readonly gray: this; + + /* + Alias for `blackBright`. + */ + readonly grey: this; + + readonly blackBright: this; + readonly redBright: this; + readonly greenBright: this; + readonly yellowBright: this; + readonly blueBright: this; + readonly magentaBright: this; + readonly cyanBright: this; + readonly whiteBright: this; + + readonly bgBlack: this; + readonly bgRed: this; + readonly bgGreen: this; + readonly bgYellow: this; + readonly bgBlue: this; + readonly bgMagenta: this; + readonly bgCyan: this; + readonly bgWhite: this; + + /* + Alias for `bgBlackBright`. + */ + readonly bgGray: this; + + /* + Alias for `bgBlackBright`. + */ + readonly bgGrey: this; + + readonly bgBlackBright: this; + readonly bgRedBright: this; + readonly bgGreenBright: this; + readonly bgYellowBright: this; + readonly bgBlueBright: this; + readonly bgMagentaBright: this; + readonly bgCyanBright: this; + readonly bgWhiteBright: this; +} + +/** +Main Chalk object that allows to chain styles together. + +Call the last one as a method with a string argument. + +Order doesn't matter, and later styles take precedent in case of a conflict. + +This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`. +*/ +declare const chalk: ChalkInstance; + +export const supportsColor: ColorInfo; + +export const chalkStderr: typeof chalk; +export const supportsColorStderr: typeof supportsColor; + +export { + ModifierName, ForegroundColorName, BackgroundColorName, ColorName, + modifierNames, foregroundColorNames, backgroundColorNames, colorNames, +// } from '#ansi-styles'; +} from './vendor/ansi-styles/index.js'; + +export { + ColorInfo, + ColorSupport, + ColorSupportLevel, +// } from '#supports-color'; +} from './vendor/supports-color/index.js'; + +// TODO: Remove these aliases in the next major version +/** +@deprecated Use `ModifierName` instead. + +Basic modifier names. +*/ +export type Modifiers = ModifierName; + +/** +@deprecated Use `ForegroundColorName` instead. + +Basic foreground color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ForegroundColor = ForegroundColorName; + +/** +@deprecated Use `BackgroundColorName` instead. + +Basic background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type BackgroundColor = BackgroundColorName; + +/** +@deprecated Use `ColorName` instead. + +Basic color names. The combination of foreground and background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type Color = ColorName; + +/** +@deprecated Use `modifierNames` instead. + +Basic modifier names. +*/ +export const modifiers: readonly Modifiers[]; + +/** +@deprecated Use `foregroundColorNames` instead. + +Basic foreground color names. +*/ +export const foregroundColors: readonly ForegroundColor[]; + +/** +@deprecated Use `backgroundColorNames` instead. + +Basic background color names. +*/ +export const backgroundColors: readonly BackgroundColor[]; + +/** +@deprecated Use `colorNames` instead. + +Basic color names. The combination of foreground and background color names. +*/ +export const colors: readonly Color[]; + +export default chalk; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js new file mode 100644 index 000000000..8bc993da5 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/index.js @@ -0,0 +1,225 @@ +import ansiStyles from '#ansi-styles'; +import supportsColor from '#supports-color'; +import { // eslint-disable-line import/order + stringReplaceAll, + stringEncaseCRLFWithFirstIndex, +} from './utilities.js'; + +const {stdout: stdoutColor, stderr: stderrColor} = supportsColor; + +const GENERATOR = Symbol('GENERATOR'); +const STYLER = Symbol('STYLER'); +const IS_EMPTY = Symbol('IS_EMPTY'); + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = [ + 'ansi', + 'ansi', + 'ansi256', + 'ansi16m', +]; + +const styles = Object.create(null); + +const applyOptions = (object, options = {}) => { + if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) { + throw new Error('The `level` option should be an integer from 0 to 3'); + } + + // Detect level if not set manually + const colorLevel = stdoutColor ? stdoutColor.level : 0; + object.level = options.level === undefined ? colorLevel : options.level; +}; + +export class Chalk { + constructor(options) { + // eslint-disable-next-line no-constructor-return + return chalkFactory(options); + } +} + +const chalkFactory = options => { + const chalk = (...strings) => strings.join(' '); + applyOptions(chalk, options); + + Object.setPrototypeOf(chalk, createChalk.prototype); + + return chalk; +}; + +function createChalk(options) { + return chalkFactory(options); +} + +Object.setPrototypeOf(createChalk.prototype, Function.prototype); + +for (const [styleName, style] of Object.entries(ansiStyles)) { + styles[styleName] = { + get() { + const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]); + Object.defineProperty(this, styleName, {value: builder}); + return builder; + }, + }; +} + +styles.visible = { + get() { + const builder = createBuilder(this, this[STYLER], true); + Object.defineProperty(this, 'visible', {value: builder}); + return builder; + }, +}; + +const getModelAnsi = (model, level, type, ...arguments_) => { + if (model === 'rgb') { + if (level === 'ansi16m') { + return ansiStyles[type].ansi16m(...arguments_); + } + + if (level === 'ansi256') { + return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_)); + } + + return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_)); + } + + if (model === 'hex') { + return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_)); + } + + return ansiStyles[type][model](...arguments_); +}; + +const usedModels = ['rgb', 'hex', 'ansi256']; + +for (const model of usedModels) { + styles[model] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]); + return createBuilder(this, styler, this[IS_EMPTY]); + }; + }, + }; + + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]); + return createBuilder(this, styler, this[IS_EMPTY]); + }; + }, + }; +} + +const proto = Object.defineProperties(() => {}, { + ...styles, + level: { + enumerable: true, + get() { + return this[GENERATOR].level; + }, + set(level) { + this[GENERATOR].level = level; + }, + }, +}); + +const createStyler = (open, close, parent) => { + let openAll; + let closeAll; + if (parent === undefined) { + openAll = open; + closeAll = close; + } else { + openAll = parent.openAll + open; + closeAll = close + parent.closeAll; + } + + return { + open, + close, + openAll, + closeAll, + parent, + }; +}; + +const createBuilder = (self, _styler, _isEmpty) => { + // Single argument is hot path, implicit coercion is faster than anything + // eslint-disable-next-line no-implicit-coercion + const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' ')); + + // We alter the prototype because we must return a function, but there is + // no way to create a function with a different prototype + Object.setPrototypeOf(builder, proto); + + builder[GENERATOR] = self; + builder[STYLER] = _styler; + builder[IS_EMPTY] = _isEmpty; + + return builder; +}; + +const applyStyle = (self, string) => { + if (self.level <= 0 || !string) { + return self[IS_EMPTY] ? '' : string; + } + + let styler = self[STYLER]; + + if (styler === undefined) { + return string; + } + + const {openAll, closeAll} = styler; + if (string.includes('\u001B')) { + while (styler !== undefined) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + string = stringReplaceAll(string, styler.close, styler.open); + + styler = styler.parent; + } + } + + // We can move both next actions out of loop, because remaining actions in loop won't have + // any/visible effect on parts we add here. Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92 + const lfIndex = string.indexOf('\n'); + if (lfIndex !== -1) { + string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + } + + return openAll + string + closeAll; +}; + +Object.defineProperties(createChalk.prototype, styles); + +const chalk = createChalk(); +export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0}); + +export { + modifierNames, + foregroundColorNames, + backgroundColorNames, + colorNames, + + // TODO: Remove these aliases in the next major version + modifierNames as modifiers, + foregroundColorNames as foregroundColors, + backgroundColorNames as backgroundColors, + colorNames as colors, +} from './vendor/ansi-styles/index.js'; + +export { + stdoutColor as supportsColor, + stderrColor as supportsColorStderr, +}; + +export default chalk; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js new file mode 100644 index 000000000..4366dee0d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/utilities.js @@ -0,0 +1,33 @@ +// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`. +export function stringReplaceAll(string, substring, replacer) { + let index = string.indexOf(substring); + if (index === -1) { + return string; + } + + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ''; + do { + returnValue += string.slice(endIndex, index) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + + returnValue += string.slice(endIndex); + return returnValue; +} + +export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) { + let endIndex = 0; + let returnValue = ''; + do { + const gotCR = string[index - 1] === '\r'; + returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix; + endIndex = index + 1; + index = string.indexOf('\n', endIndex); + } while (index !== -1); + + returnValue += string.slice(endIndex); + return returnValue; +} diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts new file mode 100644 index 000000000..58f133abe --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.d.ts @@ -0,0 +1,236 @@ +export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention + /** + The ANSI terminal control sequence for starting this style. + */ + readonly open: string; + + /** + The ANSI terminal control sequence for ending this style. + */ + readonly close: string; +} + +export interface ColorBase { + /** + The ANSI terminal control sequence for ending this color. + */ + readonly close: string; + + ansi(code: number): string; + + ansi256(code: number): string; + + ansi16m(red: number, green: number, blue: number): string; +} + +export interface Modifier { + /** + Resets the current color chain. + */ + readonly reset: CSPair; + + /** + Make text bold. + */ + readonly bold: CSPair; + + /** + Emitting only a small amount of light. + */ + readonly dim: CSPair; + + /** + Make text italic. (Not widely supported) + */ + readonly italic: CSPair; + + /** + Make text underline. (Not widely supported) + */ + readonly underline: CSPair; + + /** + Make text overline. + + Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash. + */ + readonly overline: CSPair; + + /** + Inverse background and foreground colors. + */ + readonly inverse: CSPair; + + /** + Prints the text, but makes it invisible. + */ + readonly hidden: CSPair; + + /** + Puts a horizontal line through the center of the text. (Not widely supported) + */ + readonly strikethrough: CSPair; +} + +export interface ForegroundColor { + readonly black: CSPair; + readonly red: CSPair; + readonly green: CSPair; + readonly yellow: CSPair; + readonly blue: CSPair; + readonly cyan: CSPair; + readonly magenta: CSPair; + readonly white: CSPair; + + /** + Alias for `blackBright`. + */ + readonly gray: CSPair; + + /** + Alias for `blackBright`. + */ + readonly grey: CSPair; + + readonly blackBright: CSPair; + readonly redBright: CSPair; + readonly greenBright: CSPair; + readonly yellowBright: CSPair; + readonly blueBright: CSPair; + readonly cyanBright: CSPair; + readonly magentaBright: CSPair; + readonly whiteBright: CSPair; +} + +export interface BackgroundColor { + readonly bgBlack: CSPair; + readonly bgRed: CSPair; + readonly bgGreen: CSPair; + readonly bgYellow: CSPair; + readonly bgBlue: CSPair; + readonly bgCyan: CSPair; + readonly bgMagenta: CSPair; + readonly bgWhite: CSPair; + + /** + Alias for `bgBlackBright`. + */ + readonly bgGray: CSPair; + + /** + Alias for `bgBlackBright`. + */ + readonly bgGrey: CSPair; + + readonly bgBlackBright: CSPair; + readonly bgRedBright: CSPair; + readonly bgGreenBright: CSPair; + readonly bgYellowBright: CSPair; + readonly bgBlueBright: CSPair; + readonly bgCyanBright: CSPair; + readonly bgMagentaBright: CSPair; + readonly bgWhiteBright: CSPair; +} + +export interface ConvertColor { + /** + Convert from the RGB color space to the ANSI 256 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi256(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the RGB color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToRgb(hex: string): [red: number, green: number, blue: number]; + + /** + Convert from the RGB HEX color space to the ANSI 256 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi256(hex: string): number; + + /** + Convert from the ANSI 256 color space to the ANSI 16 color space. + + @param code - A number representing the ANSI 256 color. + */ + ansi256ToAnsi(code: number): number; + + /** + Convert from the RGB color space to the ANSI 16 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the ANSI 16 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi(hex: string): number; +} + +/** +Basic modifier names. +*/ +export type ModifierName = keyof Modifier; + +/** +Basic foreground color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ForegroundColorName = keyof ForegroundColor; + +/** +Basic background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type BackgroundColorName = keyof BackgroundColor; + +/** +Basic color names. The combination of foreground and background color names. + +[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support) +*/ +export type ColorName = ForegroundColorName | BackgroundColorName; + +/** +Basic modifier names. +*/ +export const modifierNames: readonly ModifierName[]; + +/** +Basic foreground color names. +*/ +export const foregroundColorNames: readonly ForegroundColorName[]; + +/** +Basic background color names. +*/ +export const backgroundColorNames: readonly BackgroundColorName[]; + +/* +Basic color names. The combination of foreground and background color names. +*/ +export const colorNames: readonly ColorName[]; + +declare const ansiStyles: { + readonly modifier: Modifier; + readonly color: ColorBase & ForegroundColor; + readonly bgColor: ColorBase & BackgroundColor; + readonly codes: ReadonlyMap; +} & ForegroundColor & BackgroundColor & Modifier & ConvertColor; + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js new file mode 100644 index 000000000..eaa7bed6c --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/ansi-styles/index.js @@ -0,0 +1,223 @@ +const ANSI_BACKGROUND_OFFSET = 10; + +const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`; + +const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`; + +const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`; + +const styles = { + modifier: { + reset: [0, 0], + // 21 isn't widely supported and 22 does the same thing + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + overline: [53, 55], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29], + }, + color: { + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + + // Bright color + blackBright: [90, 39], + gray: [90, 39], // Alias of `blackBright` + grey: [90, 39], // Alias of `blackBright` + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39], + }, + bgColor: { + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + + // Bright color + bgBlackBright: [100, 49], + bgGray: [100, 49], // Alias of `bgBlackBright` + bgGrey: [100, 49], // Alias of `bgBlackBright` + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49], + }, +}; + +export const modifierNames = Object.keys(styles.modifier); +export const foregroundColorNames = Object.keys(styles.color); +export const backgroundColorNames = Object.keys(styles.bgColor); +export const colorNames = [...foregroundColorNames, ...backgroundColorNames]; + +function assembleStyles() { + const codes = new Map(); + + for (const [groupName, group] of Object.entries(styles)) { + for (const [styleName, style] of Object.entries(group)) { + styles[styleName] = { + open: `\u001B[${style[0]}m`, + close: `\u001B[${style[1]}m`, + }; + + group[styleName] = styles[styleName]; + + codes.set(style[0], style[1]); + } + + Object.defineProperty(styles, groupName, { + value: group, + enumerable: false, + }); + } + + Object.defineProperty(styles, 'codes', { + value: codes, + enumerable: false, + }); + + styles.color.close = '\u001B[39m'; + styles.bgColor.close = '\u001B[49m'; + + styles.color.ansi = wrapAnsi16(); + styles.color.ansi256 = wrapAnsi256(); + styles.color.ansi16m = wrapAnsi16m(); + styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET); + + // From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js + Object.defineProperties(styles, { + rgbToAnsi256: { + value(red, green, blue) { + // We use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (red === green && green === blue) { + if (red < 8) { + return 16; + } + + if (red > 248) { + return 231; + } + + return Math.round(((red - 8) / 247) * 24) + 232; + } + + return 16 + + (36 * Math.round(red / 255 * 5)) + + (6 * Math.round(green / 255 * 5)) + + Math.round(blue / 255 * 5); + }, + enumerable: false, + }, + hexToRgb: { + value(hex) { + const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); + if (!matches) { + return [0, 0, 0]; + } + + let [colorString] = matches; + + if (colorString.length === 3) { + colorString = [...colorString].map(character => character + character).join(''); + } + + const integer = Number.parseInt(colorString, 16); + + return [ + /* eslint-disable no-bitwise */ + (integer >> 16) & 0xFF, + (integer >> 8) & 0xFF, + integer & 0xFF, + /* eslint-enable no-bitwise */ + ]; + }, + enumerable: false, + }, + hexToAnsi256: { + value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)), + enumerable: false, + }, + ansi256ToAnsi: { + value(code) { + if (code < 8) { + return 30 + code; + } + + if (code < 16) { + return 90 + (code - 8); + } + + let red; + let green; + let blue; + + if (code >= 232) { + red = (((code - 232) * 10) + 8) / 255; + green = red; + blue = red; + } else { + code -= 16; + + const remainder = code % 36; + + red = Math.floor(code / 36) / 5; + green = Math.floor(remainder / 6) / 5; + blue = (remainder % 6) / 5; + } + + const value = Math.max(red, green, blue) * 2; + + if (value === 0) { + return 30; + } + + // eslint-disable-next-line no-bitwise + let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red)); + + if (value === 2) { + result += 60; + } + + return result; + }, + enumerable: false, + }, + rgbToAnsi: { + value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)), + enumerable: false, + }, + hexToAnsi: { + value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)), + enumerable: false, + }, + }); + + return styles; +} + +const ansiStyles = assembleStyles(); + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts new file mode 100644 index 000000000..0e8a9ccb8 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.d.ts @@ -0,0 +1 @@ +export {default} from './index.js'; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js new file mode 100644 index 000000000..fbb6ce0fc --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/browser.js @@ -0,0 +1,34 @@ +/* eslint-env browser */ + +const level = (() => { + if (!('navigator' in globalThis)) { + return 0; + } + + if (globalThis.navigator.userAgentData) { + const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium'); + if (brand && brand.version > 93) { + return 3; + } + } + + if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) { + return 1; + } + + return 0; +})(); + +const colorSupport = level !== 0 && { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, +}; + +const supportsColor = { + stdout: colorSupport, + stderr: colorSupport, +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts new file mode 100644 index 000000000..db44a7851 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.d.ts @@ -0,0 +1,55 @@ +import type {WriteStream} from 'node:tty'; + +export type Options = { + /** + Whether `process.argv` should be sniffed for `--color` and `--no-color` flags. + + @default true + */ + readonly sniffFlags?: boolean; +}; + +/** +Levels: +- `0` - All colors disabled. +- `1` - Basic 16 colors support. +- `2` - ANSI 256 colors support. +- `3` - Truecolor 16 million colors support. +*/ +export type ColorSupportLevel = 0 | 1 | 2 | 3; + +/** +Detect whether the terminal supports color. +*/ +export type ColorSupport = { + /** + The color level. + */ + level: ColorSupportLevel; + + /** + Whether basic 16 colors are supported. + */ + hasBasic: boolean; + + /** + Whether ANSI 256 colors are supported. + */ + has256: boolean; + + /** + Whether Truecolor 16 million colors are supported. + */ + has16m: boolean; +}; + +export type ColorInfo = ColorSupport | false; + +export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo; + +declare const supportsColor: { + stdout: ColorInfo; + stderr: ColorInfo; +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js new file mode 100644 index 000000000..265d7f858 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/node_modules/chalk/source/vendor/supports-color/index.js @@ -0,0 +1,190 @@ +import process from 'node:process'; +import os from 'node:os'; +import tty from 'node:tty'; + +// From: https://github.com/sindresorhus/has-flag/blob/main/index.js +/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) { +function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) { + const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--'); + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf('--'); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +} + +const {env} = process; + +let flagForceColor; +if ( + hasFlag('no-color') + || hasFlag('no-colors') + || hasFlag('color=false') + || hasFlag('color=never') +) { + flagForceColor = 0; +} else if ( + hasFlag('color') + || hasFlag('colors') + || hasFlag('color=true') + || hasFlag('color=always') +) { + flagForceColor = 1; +} + +function envForceColor() { + if ('FORCE_COLOR' in env) { + if (env.FORCE_COLOR === 'true') { + return 1; + } + + if (env.FORCE_COLOR === 'false') { + return 0; + } + + return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3); + } +} + +function translateLevel(level) { + if (level === 0) { + return false; + } + + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3, + }; +} + +function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) { + const noFlagForceColor = envForceColor(); + if (noFlagForceColor !== undefined) { + flagForceColor = noFlagForceColor; + } + + const forceColor = sniffFlags ? flagForceColor : noFlagForceColor; + + if (forceColor === 0) { + return 0; + } + + if (sniffFlags) { + if (hasFlag('color=16m') + || hasFlag('color=full') + || hasFlag('color=truecolor')) { + return 3; + } + + if (hasFlag('color=256')) { + return 2; + } + } + + // Check for Azure DevOps pipelines. + // Has to be above the `!streamIsTTY` check. + if ('TF_BUILD' in env && 'AGENT_NAME' in env) { + return 1; + } + + if (haveStream && !streamIsTTY && forceColor === undefined) { + return 0; + } + + const min = forceColor || 0; + + if (env.TERM === 'dumb') { + return min; + } + + if (process.platform === 'win32') { + // Windows 10 build 10586 is the first Windows release that supports 256 colors. + // Windows 10 build 14931 is the first release that supports 16m/TrueColor. + const osRelease = os.release().split('.'); + if ( + Number(osRelease[0]) >= 10 + && Number(osRelease[2]) >= 10_586 + ) { + return Number(osRelease[2]) >= 14_931 ? 3 : 2; + } + + return 1; + } + + if ('CI' in env) { + if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) { + return 3; + } + + if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + + return min; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } + + if (env.COLORTERM === 'truecolor') { + return 3; + } + + if (env.TERM === 'xterm-kitty') { + return 3; + } + + if (env.TERM === 'xterm-ghostty') { + return 3; + } + + if (env.TERM === 'wezterm') { + return 3; + } + + if ('TERM_PROGRAM' in env) { + const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': { + return version >= 3 ? 3 : 2; + } + + case 'Apple_Terminal': { + return 2; + } + // No default + } + } + + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } + + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } + + if ('COLORTERM' in env) { + return 1; + } + + return min; +} + +export function createSupportsColor(stream, options = {}) { + const level = _supportsColor(stream, { + streamIsTTY: stream && stream.isTTY, + ...options, + }); + + return translateLevel(level); +} + +const supportsColor = { + stdout: createSupportsColor({isTTY: tty.isatty(1)}), + stderr: createSupportsColor({isTTY: tty.isatty(2)}), +}; + +export default supportsColor; diff --git a/tests/fixtures/npm/cases/chalk5-builder/package.json b/tests/fixtures/npm/cases/chalk5-builder/package.json new file mode 100644 index 000000000..3804daa8d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5-builder/package.json @@ -0,0 +1 @@ +{ "name": "chalk5-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/main.ts b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts new file mode 100644 index 000000000..d17ce1167 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/main.ts @@ -0,0 +1,80 @@ +// The hot-shots StatsD client (compiled from scriptc's built-in shim) in +// buffered mode: metrics join one datagram until maxBufferSize, close +// flushes the rest, child clients share the buffer. Also explicit sample +// rates of 1, the telegraf line format, stat arrays with a callback, and +// suffixes. +import * as dgram from "node:dgram"; +import { StatsD } from "hot-shots"; + +const server = dgram.createSocket("udp4"); +const received: string[] = []; +let waiting: (() => void) | null = null; +let want = 0; +server.on("message", (msg) => { + received.push(msg.toString()); + if (waiting !== null && received.length >= want) { + const done = waiting; + waiting = null; + done(); + } +}); + +function datagrams(count: number): Promise { + want = count; + return new Promise((resolve) => { + if (received.length >= want) resolve(); + else waiting = resolve; + }); +} + +function flush(label: string): void { + console.log("--", label); + for (const line of received.splice(0)) console.log(JSON.stringify(line)); +} + +async function run(port: number): Promise { + const buffered = new StatsD({ + host: "127.0.0.1", + port, + maxBufferSize: 64, + bufferFlushInterval: 60000, + suffix: ".s", + globalTags: ["team:core"], + }); + buffered.increment("one"); + buffered.increment("two", 2); + buffered.gauge("three", 3); + buffered.histogram("four", 4, ["h:1"]); + const child = buffered.childClient({ prefix: "kid.", suffix: ".x" }); + child.increment("five"); + await new Promise((resolve) => buffered.close(() => resolve())); + await datagrams(2); + flush("buffered"); + + const telegraf = new StatsD({ host: "127.0.0.1", port, telegraf: true, globalTags: { region: "eu" } }); + telegraf.increment("hits", 1, 1, { route: "/x" }); + telegraf.timing("lat", 7, ["kind:a"]); + await datagrams(2); + flush("telegraf"); + + const plain = new StatsD({ host: "127.0.0.1", port, prefix: "p." }); + await new Promise((resolve) => { + plain.increment(["a", "b", "c"], 1, 1, ["t:1"], (err: unknown, bytes: unknown) => { + console.log("array sent", err, bytes); + resolve(); + }); + }); + plain.set("users", 42, 1); + plain.unique("visitors", "v1"); + plain.decrement("down", 3, ["d:1"]); + plain.decrement("gone"); + await datagrams(7); + flush("plain"); + await new Promise((resolve) => { + telegraf.close(() => plain.close(() => resolve())); + }); +} + +server.bind(0, "127.0.0.1", () => { + void run(server.address().port).then(() => server.close(() => console.log("server closed"))); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-buffered/package.json b/tests/fixtures/npm/cases/hot-shots-buffered/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-buffered/package.json @@ -0,0 +1 @@ +{ "type": "module" } From 6de7864697c5b49f90be08694b6104b2987d0b58 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 22:00:42 -0700 Subject: [PATCH 017/191] Record order baselines for the new chalk and hot-shots npm cases Co-Authored-By: Claude Opus 5.5 --- .../tests/fixtures/npm/cases/chalk5-builder/main.ts.json | 6 ++++++ .../fixtures/npm/cases/hot-shots-buffered/main.ts.json | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json new file mode 100644 index 000000000..545c4d701 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/chalk5-builder/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json new file mode 100644 index 000000000..e60660c09 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/hot-shots-buffered/main.ts" + ], + "diags": [] +} From 284517accbfd37cd06c87a5a352fe8b43acf10f8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 22:04:27 -0700 Subject: [PATCH 018/191] Call chalk builders with one string through a typed method A callable class's %call1 method takes a single string argument directly, skipping the variadic closure's checked-dynamic call. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/chalk/v5.ts | 8 ++++ .../src/frontend/lowering/lower-calls.ts | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/compiler/shims/chalk/v5.ts b/packages/compiler/shims/chalk/v5.ts index 6599e35f9..494a9c69f 100644 --- a/packages/compiler/shims/chalk/v5.ts +++ b/packages/compiler/shims/chalk/v5.ts @@ -435,12 +435,14 @@ export class ChalkInstance { box: LevelBox; styler: Styler | undefined; isEmpty: boolean; + private readonly root: boolean; private readonly cache: (ChalkInstance | undefined)[] = []; constructor(box: LevelBox, styler: Styler | undefined, isEmpty: boolean, root: boolean) { this.box = box; this.styler = styler; this.isEmpty = isEmpty; + this.root = root; // One unconditional store: the field's definite assignment. this["%call"] = (...text: unknown[]): string => root @@ -448,6 +450,12 @@ export class ChalkInstance { : applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text)); } + /** One string argument: the compiler calls this instead of `%call` + * (no variadic arguments to box). */ + "%call1"(text: string): string { + return this.root ? text : applyStyle(this, text); + } + get level(): ColorSupportLevel { return this.box.level as ColorSupportLevel; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 3c04924c0..9d6f477f6 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1632,6 +1632,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // A getter returning a callable class instance (chalk's // `chalk.red("x")`): call through its `%call` field. if (callee.type.kind === "object") { + const direct = callableInstanceStringCall(lowerer, expr, callee, locOf(expr)); + if (direct) return direct; callee = lowerer.hybridCallUnwrap(callee); if (callee.type.kind === "func") return lowerCallThroughValue(lowerer, expr, callee, locOf(expr)); } @@ -1886,11 +1888,47 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // a non-func IR type was already rejected while lowering the callee. // HYBRID (function-with-properties) values call through their %call slot. let callee = lowerer.lowerExpr(expr.expression); + { + const direct = callableInstanceStringCall(lowerer, expr, callee, loc); + if (direct) return direct; + } if (callee.type.kind === "record" || callee.type.kind === "object") callee = lowerer.hybridCallUnwrap(callee); return lowerCallThroughValue(lowerer, expr, callee, loc); } +/** A callable class instance called with one string: its `%call1` method + * when the class has one (the chalk shim's typed fast path), skipping the + * variadic closure's checked-dynamic arguments. */ +function callableInstanceStringCall( + lowerer: Lowerer, + expr: ts.CallExpression, + callee: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (callee.type.kind !== "object" || expr.arguments.nodes.length !== 1) return null; + const arg = expr.arguments.nodes[0]!; + if (ts.isSpreadElement(arg)) return null; + const info = lowerer.classes.get(callee.type.className); + if (info === undefined || !info.fields.has("%call") || !lowerer.findMethodOn(info, "%call1")) + return null; + const value = lowerer.lowerExpr(arg); + if (value.type.kind === "string") + return lowerer.accessorCall(callee.type.className, "%call1", callee, [value], STRING, loc); + // Any other argument (a number behind a cast): the variadic closure, + // through the checked-dynamic call as lowerCallThroughValue would. + const closure = lowerer.hybridCallUnwrap(callee); + if (closure.type.kind !== "func") return null; + return { + kind: "dynCall", + callee: { kind: "dynFrom", value: closure, type: DYN, loc }, + calleeName: ts.isIdentifier(expr.expression) ? expr.expression.text : "value", + args: [lowerer.coerceToExpected(value, DYN)], + type: DYN, + loc, + }; +} + /** A call through an evaluated callee value: checked-dynamic callees, * function unions, typed-rest and runtime-arity closures. */ function lowerCallThroughValue( @@ -9208,6 +9246,10 @@ export function lowerRecordFieldCall( // A HYBRID (function-with-properties) field is callable through its // reserved %call slot — `colors.blue("x")` where blue also carries // `.bold` (the chalk shape). + if (callee.type.kind === "object" && !init) { + const direct = callableInstanceStringCall(lowerer, call, callee, loc); + if (direct) return direct; + } if (callee.type.kind === "record" || callee.type.kind === "object") callee = lowerer.hybridCallUnwrap(callee); // Inferred JavaScript option records can retain checked callables in From 88f9ba91eb94d63c254bc934beaaeb5dd887ea7e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 22:08:46 -0700 Subject: [PATCH 019/191] Format the shims and scope the chalk declaration-merge lint exception Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/chalk/v5.ts | 241 +++++++++++++----- packages/compiler/shims/hot-shots/client.ts | 85 +++++- .../src/frontend/lowering/lower-calls.ts | 3 +- .../compiler/src/frontend/lowering/lowerer.ts | 5 +- .../compiler/src/frontend/package-shims.ts | 8 +- 5 files changed, 260 insertions(+), 82 deletions(-) diff --git a/packages/compiler/shims/chalk/v5.ts b/packages/compiler/shims/chalk/v5.ts index 494a9c69f..2d34c5c53 100644 --- a/packages/compiler/shims/chalk/v5.ts +++ b/packages/compiler/shims/chalk/v5.ts @@ -138,10 +138,7 @@ export const backgroundColorNames: BackgroundColorName[] = [ "bgCyanBright", "bgWhiteBright", ]; -export const colorNames: ColorName[] = [ - ...foregroundColorNames, - ...backgroundColorNames, -]; +export const colorNames: ColorName[] = [...foregroundColorNames, ...backgroundColorNames]; export const modifiers: Modifiers[] = modifierNames; export const foregroundColors: ForegroundColor[] = foregroundColorNames; export const backgroundColors: BackgroundColor[] = backgroundColorNames; @@ -151,14 +148,12 @@ const ESC = "\u001B["; /** Open and close codes, indexed by style id (the order of the getters). */ const OPEN_CODES: number[] = [ - 0, 1, 2, 3, 4, 53, 7, 8, 9, - 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96, 97, - 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107, + 0, 1, 2, 3, 4, 53, 7, 8, 9, 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96, + 97, 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107, ]; const CLOSE_CODES: number[] = [ - 0, 22, 22, 23, 24, 55, 27, 28, 29, - 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, - 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, + 0, 22, 22, 23, 24, 55, 27, 28, 29, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, ]; const VISIBLE = 45; @@ -238,9 +233,19 @@ function hasFlag(flag: string): boolean { } let flagForceColor: number | undefined = undefined; -if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) { +if ( + hasFlag("no-color") || + hasFlag("no-colors") || + hasFlag("color=false") || + hasFlag("color=never") +) { flagForceColor = 0; -} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) { +} else if ( + hasFlag("color") || + hasFlag("colors") || + hasFlag("color=true") || + hasFlag("color=always") +) { flagForceColor = 1; } @@ -277,7 +282,8 @@ function detectLevel(streamIsTTY: boolean): number { if (hasFlag("color=256")) return 2; if (inEnv("TF_BUILD") && inEnv("AGENT_NAME")) return 1; if (!streamIsTTY && forceColor === undefined) return 0; - const min = forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor; + const min = + forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor; const env = process.env; const term = env["TERM"]; if (term === "dumb") return min; @@ -388,7 +394,10 @@ function stringEncaseCRLFWithFirstIndex( do { const gotCR = string[index - 1] === "\r"; returnValue += - string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix; + string.slice(endIndex, gotCR ? index - 1 : index) + + prefix + + (gotCR ? "\r\n" : "\n") + + postfix; endIndex = index + 1; index = string.indexOf("\n", endIndex); } while (index !== -1); @@ -426,12 +435,19 @@ function joinArguments(args: unknown[]): string { /** A chalk instance or one of its style chains: callable (chalk's call * signature merges in from the interface), with every style a cached * getter. */ +// The merge is the point: the interface gives instances chalk's call +// signature, and the class's `%call` field implements it. +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging export interface ChalkInstance { (...text: unknown[]): string; } +// oxlint-disable-next-line typescript/no-unsafe-declaration-merging export class ChalkInstance { - "%call": (...text: unknown[]) => string; + "%call": (...text: unknown[]) => string = (...text: unknown[]): string => + this.root + ? joinArguments(text) + : applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text)); box: LevelBox; styler: Styler | undefined; isEmpty: boolean; @@ -443,11 +459,6 @@ export class ChalkInstance { this.styler = styler; this.isEmpty = isEmpty; this.root = root; - // One unconditional store: the field's definite assignment. - this["%call"] = (...text: unknown[]): string => - root - ? joinArguments(text) - : applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text)); } /** One string argument: the compiler calls this instead of `%call` @@ -509,57 +520,153 @@ export class ChalkInstance { return this.model(ansi256Open(10, index), true); } - get reset(): ChalkInstance { return this.style(0); } - get bold(): ChalkInstance { return this.style(1); } - get dim(): ChalkInstance { return this.style(2); } - get italic(): ChalkInstance { return this.style(3); } - get underline(): ChalkInstance { return this.style(4); } - get overline(): ChalkInstance { return this.style(5); } - get inverse(): ChalkInstance { return this.style(6); } - get hidden(): ChalkInstance { return this.style(7); } - get strikethrough(): ChalkInstance { return this.style(8); } - get visible(): ChalkInstance { return this.style(VISIBLE); } - get black(): ChalkInstance { return this.style(9); } - get red(): ChalkInstance { return this.style(10); } - get green(): ChalkInstance { return this.style(11); } - get yellow(): ChalkInstance { return this.style(12); } - get blue(): ChalkInstance { return this.style(13); } - get magenta(): ChalkInstance { return this.style(14); } - get cyan(): ChalkInstance { return this.style(15); } - get white(): ChalkInstance { return this.style(16); } - get blackBright(): ChalkInstance { return this.style(17); } - get gray(): ChalkInstance { return this.style(18); } - get grey(): ChalkInstance { return this.style(19); } - get redBright(): ChalkInstance { return this.style(20); } - get greenBright(): ChalkInstance { return this.style(21); } - get yellowBright(): ChalkInstance { return this.style(22); } - get blueBright(): ChalkInstance { return this.style(23); } - get magentaBright(): ChalkInstance { return this.style(24); } - get cyanBright(): ChalkInstance { return this.style(25); } - get whiteBright(): ChalkInstance { return this.style(26); } - get bgBlack(): ChalkInstance { return this.style(27); } - get bgRed(): ChalkInstance { return this.style(28); } - get bgGreen(): ChalkInstance { return this.style(29); } - get bgYellow(): ChalkInstance { return this.style(30); } - get bgBlue(): ChalkInstance { return this.style(31); } - get bgMagenta(): ChalkInstance { return this.style(32); } - get bgCyan(): ChalkInstance { return this.style(33); } - get bgWhite(): ChalkInstance { return this.style(34); } - get bgBlackBright(): ChalkInstance { return this.style(35); } - get bgGray(): ChalkInstance { return this.style(36); } - get bgGrey(): ChalkInstance { return this.style(37); } - get bgRedBright(): ChalkInstance { return this.style(38); } - get bgGreenBright(): ChalkInstance { return this.style(39); } - get bgYellowBright(): ChalkInstance { return this.style(40); } - get bgBlueBright(): ChalkInstance { return this.style(41); } - get bgMagentaBright(): ChalkInstance { return this.style(42); } - get bgCyanBright(): ChalkInstance { return this.style(43); } - get bgWhiteBright(): ChalkInstance { return this.style(44); } + get reset(): ChalkInstance { + return this.style(0); + } + get bold(): ChalkInstance { + return this.style(1); + } + get dim(): ChalkInstance { + return this.style(2); + } + get italic(): ChalkInstance { + return this.style(3); + } + get underline(): ChalkInstance { + return this.style(4); + } + get overline(): ChalkInstance { + return this.style(5); + } + get inverse(): ChalkInstance { + return this.style(6); + } + get hidden(): ChalkInstance { + return this.style(7); + } + get strikethrough(): ChalkInstance { + return this.style(8); + } + get visible(): ChalkInstance { + return this.style(VISIBLE); + } + get black(): ChalkInstance { + return this.style(9); + } + get red(): ChalkInstance { + return this.style(10); + } + get green(): ChalkInstance { + return this.style(11); + } + get yellow(): ChalkInstance { + return this.style(12); + } + get blue(): ChalkInstance { + return this.style(13); + } + get magenta(): ChalkInstance { + return this.style(14); + } + get cyan(): ChalkInstance { + return this.style(15); + } + get white(): ChalkInstance { + return this.style(16); + } + get blackBright(): ChalkInstance { + return this.style(17); + } + get gray(): ChalkInstance { + return this.style(18); + } + get grey(): ChalkInstance { + return this.style(19); + } + get redBright(): ChalkInstance { + return this.style(20); + } + get greenBright(): ChalkInstance { + return this.style(21); + } + get yellowBright(): ChalkInstance { + return this.style(22); + } + get blueBright(): ChalkInstance { + return this.style(23); + } + get magentaBright(): ChalkInstance { + return this.style(24); + } + get cyanBright(): ChalkInstance { + return this.style(25); + } + get whiteBright(): ChalkInstance { + return this.style(26); + } + get bgBlack(): ChalkInstance { + return this.style(27); + } + get bgRed(): ChalkInstance { + return this.style(28); + } + get bgGreen(): ChalkInstance { + return this.style(29); + } + get bgYellow(): ChalkInstance { + return this.style(30); + } + get bgBlue(): ChalkInstance { + return this.style(31); + } + get bgMagenta(): ChalkInstance { + return this.style(32); + } + get bgCyan(): ChalkInstance { + return this.style(33); + } + get bgWhite(): ChalkInstance { + return this.style(34); + } + get bgBlackBright(): ChalkInstance { + return this.style(35); + } + get bgGray(): ChalkInstance { + return this.style(36); + } + get bgGrey(): ChalkInstance { + return this.style(37); + } + get bgRedBright(): ChalkInstance { + return this.style(38); + } + get bgGreenBright(): ChalkInstance { + return this.style(39); + } + get bgYellowBright(): ChalkInstance { + return this.style(40); + } + get bgBlueBright(): ChalkInstance { + return this.style(41); + } + get bgMagentaBright(): ChalkInstance { + return this.style(42); + } + get bgCyanBright(): ChalkInstance { + return this.style(43); + } + get bgWhiteBright(): ChalkInstance { + return this.style(44); + } } function optionsLevel(options: Options | undefined): number { const level = options === undefined ? undefined : options.level; - if (level !== undefined && level !== 0 && !(Number.isInteger(level) && level >= 0 && level <= 3)) { + if ( + level !== undefined && + level !== 0 && + !(Number.isInteger(level) && level >= 0 && level <= 3) + ) { throw new Error("The `level` option should be an integer from 0 to 3"); } if (level === undefined) return stdoutColor === false ? 0 : stdoutColor.level; diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts index d0b5c8924..2f8ca0580 100644 --- a/packages/compiler/shims/hot-shots/client.ts +++ b/packages/compiler/shims/hot-shots/client.ts @@ -542,7 +542,13 @@ export class StatsD { // ── stats functions ── - timing(stat: string | string[], time: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + timing( + stat: string | string[], + time: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; timing(stat: string | string[], time: number, tags?: Tags, callback?: StatsCb): void; timing(stat: string | string[], time: number, callback?: StatsCb): void; timing(stat: string | string[], time: number, sampleRate?: number, callback?: StatsCb): void; @@ -557,7 +563,13 @@ export class StatsD { } increment(stat: string, tags?: Tags): void; - increment(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + increment( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; increment(stat: string | string[], value: number, callback?: StatsCb): void; increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; @@ -580,7 +592,13 @@ export class StatsD { } decrement(stat: string, tags?: Tags): void; - decrement(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + decrement( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; decrement(stat: string | string[], value: number, callback?: StatsCb): void; decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; @@ -601,7 +619,13 @@ export class StatsD { this.sendAll(stat, -amount, "c", sampleRate, tagArg, callback); } - histogram(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + histogram( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; histogram(stat: string | string[], value: number, callback?: StatsCb): void; histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; @@ -615,10 +639,21 @@ export class StatsD { this.sendAll(stat, value, "h", sampleRate, tags, callback); } - distribution(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; distribution(stat: string | string[], value: number, callback?: StatsCb): void; - distribution(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; + distribution( + stat: string | string[], + value: number, + sampleRate?: number, + callback?: StatsCb, + ): void; distribution( stat: string | string[], value: number, @@ -629,7 +664,13 @@ export class StatsD { this.sendAll(stat, value, "d", sampleRate, tags, callback); } - gauge(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + gauge( + stat: string | string[], + value: number, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void; gauge(stat: string | string[], value: number, callback?: StatsCb): void; gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void; @@ -643,10 +684,21 @@ export class StatsD { this.sendAll(stat, value, "g", sampleRate, tags, callback); } - set(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; set(stat: string | string[], value: number | string, callback?: StatsCb): void; - set(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; + set( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; set( stat: string | string[], value: number | string, @@ -657,10 +709,21 @@ export class StatsD { this.sendAll(stat, value, "s", sampleRate, tags, callback); } - unique(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + tags?: Tags, + callback?: StatsCb, + ): void; unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void; unique(stat: string | string[], value: number | string, callback?: StatsCb): void; - unique(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void; + unique( + stat: string | string[], + value: number | string, + sampleRate?: number, + callback?: StatsCb, + ): void; unique( stat: string | string[], value: number | string, diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 9d6f477f6..b650ca567 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1635,7 +1635,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const direct = callableInstanceStringCall(lowerer, expr, callee, locOf(expr)); if (direct) return direct; callee = lowerer.hybridCallUnwrap(callee); - if (callee.type.kind === "func") return lowerCallThroughValue(lowerer, expr, callee, locOf(expr)); + if (callee.type.kind === "func") + return lowerCallThroughValue(lowerer, expr, callee, locOf(expr)); } if (callee.type.kind === "func") { const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr)); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ac1a778cf..206c42fdc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -9656,7 +9656,10 @@ export class Lowerer { (id) => this.unions.get(id), ) ) { - return this.coerceToExpected({ kind: "dynFrom", value: un, type: DYN, loc: un.loc }, expected); + return this.coerceToExpected( + { kind: "dynFrom", value: un, type: DYN, loc: un.loc }, + expected, + ); } } // A zero-param function whose RETURN is a wider record array than the diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 3af7ebf59..a0c2e4cb9 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -72,7 +72,10 @@ interface ImplementationShim { variants?: readonly string[]; } -const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([ +const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< + string, + ImplementationShim +>([ [ "fs-extra", { @@ -330,7 +333,8 @@ function shimOfDeclarationPath(path: string): { pkgDir: string; shim: Implementa for (const shim of IMPLEMENTATION_SHIMS.values()) { if (shim.typesFile === undefined) continue; const suffix = `/${shim.name}/${shim.typesFile}`; - if (path.endsWith(suffix)) return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim }; + if (path.endsWith(suffix)) + return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim }; } return null; } From 8c9f5cfc9ff02dccb7a97266d210bfba559a5ccc Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 01:42:01 -0700 Subject: [PATCH 020/191] Free hot-shots' unclosed udp and tcp transports at exit The socket's error listener captured the transport, so a client that is never closed (mock mode) kept a cycle the RC audit reports at exit; it now captures only the listener list. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/hot-shots/client.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts index 2f8ca0580..aa752d703 100644 --- a/packages/compiler/shims/hot-shots/client.ts +++ b/packages/compiler/shims/hot-shots/client.ts @@ -218,7 +218,11 @@ class UdpTransport implements Transport { this.cacheDnsTtl = cacheDnsTtl; this.socket = dgram.createSocket("udp4"); this.socket.unref(); - this.socket.on("error", (err) => this.errors.emit(err)); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); } send(buf: Buffer, callback: SendCallback): void { if (!this.cacheDns) { @@ -263,7 +267,11 @@ class TcpTransport implements Transport { this.socket = net.connect(port, host ?? "localhost"); this.socket.setKeepAlive(true); this.socket.unref(); - this.socket.on("error", (err) => this.errors.emit(err)); + // The listener captures the list, not the transport: a socket that is + // never closed keeps its listener, and a capture of `this` would make + // a cycle the reference counts cannot free at exit. + const errors = this.errors; + this.socket.on("error", (err) => errors.emit(err)); } send(buf: Buffer, callback: SendCallback): void { // The typed socket's write takes the data alone; the completion runs From 2e297caf9955cf8cb4606c9acd7dda8f3eac5877 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 05:33:34 -0700 Subject: [PATCH 021/191] Log hot-shots' failed final flush through an unknown value Programs whose Error subclasses define accessors still compile the shim's close logging. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/hot-shots/client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts index aa752d703..b407132f4 100644 --- a/packages/compiler/shims/hot-shots/client.ts +++ b/packages/compiler/shims/hot-shots/client.ts @@ -155,6 +155,13 @@ function formatDate(date: number): number { return Math.round(date); } +/** console.error of an arbitrary value, as hot-shots logs errors. The + * value crosses as unknown: the typed Error path has no inspect lowering + * in programs whose Error subclasses define accessors. */ +function logError(value: unknown): void { + console.error(value); +} + function errorWithCode(message: string, code: string | undefined): Error { const error: NodeJS.ErrnoException = new Error(message); error.code = code; @@ -1013,7 +1020,7 @@ export class StatsD { this.flushQueue((err) => { if (err) { if (callback) return callback(err); - console.error(err); + logError(err); return; } let intervalAttempts = 0; From 164174f811a2f0f927fde6f76f5578c6ff538139 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 02:29:47 -0700 Subject: [PATCH 022/191] Keep literal key order for records a narrowing throw arm types The key-order witness pass read every libCall typed as a record as an opaque producer of that record, which pins the declaration order. A void runtime call typed by its use site, such as error.nodeThrow in the ternary that narrows a possibly-undefined binding before a member read, never produces a value. A module-level accumulator enumerated in its interface's order instead of its literal's. New corpus program 6915. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/key-order-witness.ts | 6 +- .../6915-key-order-narrowed-throw.ts.json | 6 ++ tests/corpus/6915-key-order-narrowed-throw.ts | 72 +++++++++++++++++++ .../6915-key-order-narrowed-throw.ts.json | 1 + 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json create mode 100644 tests/corpus/6915-key-order-narrowed-throw.ts create mode 100644 tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 2fc9a6cbd..7ce8ae074 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -45,6 +45,7 @@ import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; +import { LIB_FN_SIGS } from "../../ir/builtin-signatures.js"; /** One property of an object literal, in source order. */ export type KeyOrderSegment = @@ -312,7 +313,10 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // Runtime surfaces (console, util.format, workers) that take a // record may walk its keys; runtime builders make new records. for (const arg of e.args) observedRoots.push(arg.type); - producedBy(e.type, true); + // A void runtime call typed by its use site is a throw standing in + // for a value (`error.nodeThrow` in a narrowing ternary): it never + // produces one. + if (LIB_FN_SIGS[e.fn].result.kind !== "void") producedBy(e.type, true); return; case "intrinsic": // Intrinsics hand back values built elsewhere, except allSettled's diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..83f8cc5d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6915-key-order-narrowed-throw.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6915-key-order-narrowed-throw.ts b/tests/corpus/6915-key-order-narrowed-throw.ts new file mode 100644 index 000000000..d4bc693f2 --- /dev/null +++ b/tests/corpus/6915-key-order-narrowed-throw.ts @@ -0,0 +1,72 @@ +// A record whose reads narrow a possibly-undefined binding (the narrowing +// ternary's throw arm stands in for a value of the record's type) still +// takes its literal's key order: a module-level accumulator typed by an +// interface declared in another order, as an event batcher builds it. +interface Entry { + id: string; + at: number; + text?: string; + stream: "out" | "err"; +} +interface Batch { + team: string; + project: string; + job: string; + origin: "task" | "worker"; + host: string; + entries: Entry[]; + sizes?: number[]; + stage?: "live" | "staging"; + branch?: string; + run?: string; + entry?: string; + path?: string; +} +interface Job { + id: string; + run?: string; + project: string; + team: string; + host: string; + stage: string | null; +} +let acc: Batch | undefined; + +function append(j: Job, text: string): void { + if (!acc) { + acc = { + run: j.run, + job: j.id, + stage: j.stage === null ? "staging" : "live", + project: j.project, + team: j.team, + origin: "task", + host: j.host, + entry: ".", + entries: [], + }; + } + acc.entries.push({ id: String(acc.entries.length), at: 1, text, stream: "out" }); + setTimeout(() => { + const batch = acc; + if (!batch) return; + acc = undefined; + for (const line of split(batch)) console.log(line); + }, 1); +} + +function split(batch: Batch): string[] { + const json = JSON.stringify(batch); + if (json.length < 200 || batch.entries.length < 2) return [json]; + const index = Math.floor(batch.entries.length / 2); + const first = { ...batch, entries: batch.entries.slice(0, index) }; + const second = { ...batch, entries: batch.entries.slice(index) }; + return [...split(first), ...split(second)]; +} + +const j: Job = { id: "job", run: "r1", project: "prj", team: "core", host: "h", stage: "live" }; +append(j, "one"); +append(j, "two"); +setTimeout(() => { + for (let i = 0; i < 6; i++) append(j, "line " + i); +}, 5); diff --git a/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json new file mode 100644 index 000000000..fae2adeb4 --- /dev/null +++ b/tests/harness/corpus-costs/6915-key-order-narrowed-throw.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 9000 } From dd3ea7c00c80f3fa7eb3ba30d34863a28f2ed928 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:47:03 -0500 Subject: [PATCH 023/191] 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 024/191] 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 025/191] 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 026/191] 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 712b4e84a1e52dca58f559a20123f935e68e54c5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:12:12 -0500 Subject: [PATCH 027/191] Support the env option of spawnSync `spawnSync(cmd, args, { env })` replaces the child's environment with the given record, as spawn and execFileSync already do, alone or with the timeout, killSignal and stdio options. The fallback typings declare the option. --- .../ambient/scriptc-node-fallback.d.ts | 4 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + .../lowering/builtins/child-process.ts | 19 +++++++++- .../compiler/src/ir/builtin-signatures.ts | 6 +++ .../tests/corpus/7030-spawn-sync-env.ts.json | 6 +++ packages/runtime/src/scr_child.c | 38 +++++++++++++++---- packages/runtime/src/scr_child.h | 5 +++ tests/corpus/7030-spawn-sync-env.ts | 28 ++++++++++++++ .../corpus-costs/7030-spawn-sync-env.ts.json | 1 + 10 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json create mode 100644 tests/corpus/7030-spawn-sync-env.ts create mode 100644 tests/harness/corpus-costs/7030-spawn-sync-env.ts.json diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..c5fce2423 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3512,7 +3512,8 @@ declare module "child_process" { * killSignal at the deadline and the result carries error: ETIMEDOUT + * the signal, never a throw), killSignal (a signal NAME literal), * stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures - * read "" for non-piped streams), and windowsHide (a POSIX no-op). */ + * read "" for non-piped streams), env (replaces the child's + * environment), and windowsHide (a POSIX no-op). */ export function spawnSync( command: string, args?: string[], @@ -3521,6 +3522,7 @@ declare module "child_process" { timeout?: number; killSignal?: string; stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[]; + env?: { [k: string]: string | undefined }; windowsHide?: boolean; }, ): SpawnSyncReturns; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..f18e4a7d5 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -754,6 +754,7 @@ export const LIB_FN_SYMS: Record = { "cp.spawnSync": "scr_spawn_sync", "cp.spawnSyncOpts": "scr_spawn_sync_opts", "cp.spawnSyncStdioStr": "scr_spawn_sync_stdio_str", + "cp.spawnSyncEnv": "scr_spawn_sync_env", "cp.execSync": "scr_exec_sync", "cp.execCapture": "scr_exec_capture", "spawnRes.stdout": "scr_spawn_res_stdout", diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index d31f14d12..6d1593a55 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -118,6 +118,7 @@ const INERT_LIB_CALLS = new Set([ "worker.parentPort", "worker.root", "cp.spawnSyncOpts", + "cp.spawnSyncEnv", ]); /** jsval conversions (ir/jsval.ts): no user code, no stores. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ea2dd379d..9096316a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -115,6 +115,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo // "pipe"`) — accepted when its TYPE proves every arm is a supported // literal; the runtime maps the value to the modes at the call. let stdioStr: IrExpr | null = null; + // The `env` option's [k,v,...] pairs, which replace the child's environment. + let envPairs: IrExpr | null = null; let plain = true; // no behavior-changing option: the historical libCall if (optsNode) { @@ -227,11 +229,16 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo case "windowsHide": lowerer.lowerExpr(m.value); // Node no-op on POSIX break; + case "env": + // A REPLACEMENT environment, as for spawn and execFileSync. + envPairs = recordToEnvPairs(lowerer, m.value); + plain = false; + break; default: lowerer.noLowering( `spawnSync option '${m.name}'`, p, - "encoding, timeout, killSignal, stdio, and windowsHide are the supported options", + "encoding, timeout, killSignal, stdio, env, and windowsHide are the supported options", ); } } @@ -239,6 +246,13 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo if (plain) { return { kind: "libCall", fn: "cp.spawnSync", args: [cmd, argv], type: SPAWNRES_T, loc }; } + if (stdioStr !== null && envPairs !== null) { + lowerer.noLowering( + "spawnSync with a runtime stdio value and env", + optsNode!, + 'pass stdio as a "pipe"/"ignore"/"inherit" literal when env is set', + ); + } if (stdioStr !== null) { return { kind: "libCall", @@ -250,7 +264,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo } return { kind: "libCall", - fn: "cp.spawnSyncOpts", + fn: envPairs !== null ? "cp.spawnSyncEnv" : "cp.spawnSyncOpts", args: [ cmd, argv, @@ -259,6 +273,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo numLit(inMode, loc), numLit(outMode, loc), numLit(errMode, loc), + ...(envPairs !== null ? [envPairs] : []), ], type: SPAWNRES_T, loc, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..6b7dc1741 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1318,6 +1318,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64], result: SPAWNRES_T, }, + /** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...] + * pairs replace the child's environment (recordToEnvPairs). */ + "cp.spawnSyncEnv": { + argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)], + result: SPAWNRES_T, + }, /** cp.spawnSyncOpts with the stdio carried as a RUNTIME string — * "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the * defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..f8afbe00b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7030-spawn-sync-env.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7030-spawn-sync-env.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656d..387885c42 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -1105,9 +1105,9 @@ char **scr_child_argv(ScrStr *cmd, ScrArr *args) { static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, - int err_mode) { + int err_mode, ScrArr *env_pairs) { ScrWinSyncRes w; - scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, NULL, + scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, env_pairs, timeout_ms, &w); if (w.spawn_errname != NULL) { free(w.out.data); @@ -1136,19 +1136,26 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { - return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0); + return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL); } ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode) { + return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode, + NULL); +} + +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs) { int killsig = SIGTERM; if (killsignal->len > 0) { int resolved = scr_signal_from_name(killsignal); if (resolved > 0) killsig = resolved; } return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode, - (int)out_mode, (int)err_mode); + (int)out_mode, (int)err_mode, env_pairs); } ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms, @@ -3101,9 +3108,14 @@ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status, * documented divergence). timeout_ms > 0 sends `killsig` at the deadline * and the result carries error: ETIMEDOUT + the signal — never a throw, * Node's spawnSync shape. */ +static char **scr_exec_envp(ScrArr *pairs); +static void scr_exec_envp_free(char **envp); + +/* env_pairs: NULL inherits the parent's environment; else [k,v,...] + * replaces it, as Node's `env` option does. */ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, - int err_mode) { + int err_mode, ScrArr *env_pairs) { if (out_mode == 2 || err_mode == 2) { /* Inherited output fds: flush the parent's buffers first so earlier * logs precede the child's writes (Node's practical ordering). */ @@ -3138,10 +3150,13 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } char **argv = scr_child_argv(cmd, args); + char **envp = env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ); + int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, + envp != NULL ? envp : environ); posix_spawn_file_actions_destroy(&fa); free(argv); + scr_exec_envp_free(envp); if (cap_out) close(outfd[1]); if (cap_err) close(errfd[1]); @@ -3242,7 +3257,7 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { - return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0); + return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0, NULL); } /* The options entry (cp.spawnSyncOpts): killSignal as a NAME ("" = the @@ -3252,13 +3267,20 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) { ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode) { + return scr_spawn_sync_env(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode, + NULL); +} + +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs) { int killsig = SIGTERM; if (killsignal->len > 0) { int resolved = scr_signal_from_name(killsignal); if (resolved > 0) killsig = resolved; } return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode, - (int)out_mode, (int)err_mode); + (int)out_mode, (int)err_mode, env_pairs); } /* The runtime-string stdio entry (cp.spawnSyncStdioStr): the compiler diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index e899130d8..a5853ad59 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -23,6 +23,11 @@ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args); /* +1, never throws */ ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms, ScrStr *killsignal, double in_mode, double out_mode, double err_mode); +/* scr_spawn_sync_opts with an `env` option: [k,v,...] pairs replace the + * child's environment (NULL inherits the parent's). */ +ScrSpawnRes *scr_spawn_sync_env(ScrStr *cmd, ScrArr *args, double timeout_ms, + ScrStr *killsignal, double in_mode, double out_mode, + double err_mode, ScrArr *env_pairs); /* The runtime-string stdio entry: the compiler proved the value is * "pipe" | "ignore" | "inherit" by type; the mode mapping happens here. */ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms, diff --git a/tests/corpus/7030-spawn-sync-env.ts b/tests/corpus/7030-spawn-sync-env.ts new file mode 100644 index 000000000..e613c81f0 --- /dev/null +++ b/tests/corpus/7030-spawn-sync-env.ts @@ -0,0 +1,28 @@ +// spawnSync's env option replaces the child's environment, alone or with +// the other options. +import { spawnSync } from "node:child_process"; + +const merged = spawnSync("sh", ["-c", 'echo "$GREETING $HOME_SET"'], { + env: { ...process.env, GREETING: "hello", HOME_SET: process.env.HOME ? "yes" : "no" }, + encoding: "utf8", +}); +console.log(merged.stdout.trim(), merged.status); + +const only = spawnSync("/bin/sh", ["-c", 'echo "${GREETING:-unset} ${HOME:-nohome}"'], { + env: { GREETING: "only" }, + encoding: "utf8", +}); +console.log(only.stdout.trim()); + +const empty = spawnSync("/bin/sh", ["-c", "env | grep -c ."], { env: {}, encoding: "utf8" }); +console.log(Number(empty.stdout.trim()) <= 3, empty.status); + +const quiet = spawnSync("sh", ["-c", 'echo "$MODE" >&2; exit 4'], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin", MODE: "loud" }, + stdio: ["ignore", "pipe", "pipe"], + timeout: 5000, +}); +console.log(quiet.status, quiet.stderr.toString().trim(), quiet.stdout.toString() === ""); + +const missing = spawnSync("/nonexistent/tool", [], { env: { A: "1" } }); +console.log(missing.status, missing.error?.message); diff --git a/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json new file mode 100644 index 000000000..cf8ef4d26 --- /dev/null +++ b/tests/harness/corpus-costs/7030-spawn-sync-env.ts.json @@ -0,0 +1 @@ +{ "plain": 2460, "san": 4060 } From 7259f10ad47c79d5f201502eb88208990ad82d08 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:16:36 -0500 Subject: [PATCH 028/191] Keep running maximum and minimum updates as branches With a running maximum in a register, LLVM turns `if (v > max) { max = v; at = i; }` into selects, so each element waits on the previous one's `maxsd`. The update gets rarer as a scan goes on and its branch predicts well, so such an `if` now carries branch weights that say it is rarely taken, and LLVM keeps the branch. An `if` qualifies when it has no `else`, its condition is one numeric `<`, `<=`, `>` or `>=` between a local and another expression, and it assigns that expression to the local. The local must be an accumulator: inside a loop the `if` is a statement of the innermost loop's body, the local is declared outside that loop, and the loop writes it only in this `if`; outside a loop the `if` is a statement of the function body and the local is a binding the function captures and writes only there, as in a callback. A clamp of a value read in the same iteration, or a sum clamped after each addition, is left alone: its branch goes whichever way the data goes. So is an update nested in another branch, whose kept branch can stop LLVM from turning the enclosing, data-dependent branch into selects. --- packages/compiler/src/backend/llvm/blocks.ts | 4 +- .../src/backend/llvm/emit-function.ts | 2 + .../src/backend/llvm/emit-statements.ts | 7 +- packages/compiler/src/backend/llvm/emitter.ts | 3 + .../src/backend/llvm/running-extrema.test.ts | 97 ++++++++++ .../src/backend/llvm/running-extrema.ts | 110 ++++++++++++ .../test/running-extrema-branches.test.ts | 75 ++++++++ .../7301-running-extrema-branches.ts.json | 6 + tests/corpus/7301-running-extrema-branches.ts | 169 ++++++++++++++++++ .../7301-running-extrema-branches.ts.json | 1 + 10 files changed, 471 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/running-extrema.test.ts create mode 100644 packages/compiler/src/backend/llvm/running-extrema.ts create mode 100644 packages/compiler/test/running-extrema-branches.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json create mode 100644 tests/corpus/7301-running-extrema-branches.ts create mode 100644 tests/harness/corpus-costs/7301-running-extrema-branches.ts.json diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 6d22bf3bf..354e631c6 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -95,8 +95,8 @@ export class BlockBuilder { this.terminate(`br label %${label}`); } - condBr(v: string, t: string, f: string): void { - this.terminate(`br i1 ${v}, label %${t}, label %${f}`); + condBr(v: string, t: string, f: string, attachment = ""): void { + this.terminate(`br i1 ${v}, label %${t}, label %${f}${attachment}`); } /** Emit a double-indexed `for (i = 0; i < len; i++)` loop. `next` is diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 09e6551c3..3aa9a28a7 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -55,6 +55,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 { runningExtremumUpdates } from "./running-extrema.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); @@ -81,6 +82,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.currentLocals.set(id, plain); emitter.lazyCaptureBoxes.set(id, ""); } + emitter.runningExtrema = emitter.debug === null ? runningExtremumUpdates(fn) : new Set(); emitter.currentConstantCallbacks = emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); // Preserve concrete source bindings for debugger inspection. Suspended diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7239db7d8..0227776ab 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -782,7 +782,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const trueLabel = B.newLabel("if.t"); const joinLabel = B.newLabel("if.j"); const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel; - B.condBr(cond, trueLabel, falseLabel); + // A running maximum or minimum is rarely updated; the weights keep + // the branch, where a select would make each element wait on the last. + const weights = emitter.runningExtrema.has(s) + ? `, !prof !{!"branch_weights", i32 1, i32 1000}` + : ""; + B.condBr(cond, trueLabel, falseLabel, weights); B.startBlock(trueLabel); emitter.emitBlock(s.then); B.br(joinLabel); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ed1785912..ed1029b85 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -831,6 +831,9 @@ export class LlEmitter { readonly callLifetimes: CallLifetimes; readonly constantCallbacks: Map>; currentConstantCallbacks: ReadonlyMap = new Map(); + /** The current function's running maximum and minimum updates, whose + * branches are weighted as rarely taken (running-extrema.ts). */ + runningExtrema: ReadonlySet = new Set(); readonly stackCallbacks: StackCallbacks; readonly receiverReaders: AmbientReceiverReaders; readonly stackCaptures: StackCaptures; diff --git a/packages/compiler/src/backend/llvm/running-extrema.test.ts b/packages/compiler/src/backend/llvm/running-extrema.test.ts new file mode 100644 index 000000000..4ce307c5c --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { runningExtremumUpdates } from "./running-extrema.js"; + +const loc = { file: "extrema.ts", start: 0, end: 0 }; +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const assign = (localId: string, value: IrExpr): IrStmt => ({ + kind: "assign", + localId, + value, + loc, +}); +const decl = (localId: string): IrStmt => ({ kind: "varDecl", localId, init: num(0), loc }); +const compare = (op: "<" | ">", left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left, + right, + type: BOOL, + loc, +}); +/** `if (v > max) max = v;` */ +const update = (): IrStmt & { kind: "if" } => ({ + kind: "if", + cond: compare(">", ref("v"), ref("max")), + then: [assign("max", ref("v"))], + else_: null, + loc, +}); +const loop = (body: IrStmt[]): IrStmt => ({ + kind: "while", + cond: compare(">", ref("n"), num(0)), + body, + loc, +}); +const fn = (body: IrStmt[], captures: string[] = []): IrFunction => ({ + name: "scan", + params: [], + locals: ["v", "max", "n"].map((id) => ({ id, name: id, type: F64, mutable: true })), + captures: captures.map((localId) => ({ localId, name: localId, type: F64 })), + returnType: F64, + body, + loc, +}); + +test("a loop's update of an accumulator declared outside it qualifies", () => { + const s = update(); + expect(runningExtremumUpdates(fn([decl("max"), loop([decl("v"), s])])).has(s)).toBe(true); +}); + +test("the local may be on either side of the comparison", () => { + const s = update(); + s.cond = compare("<", ref("max"), ref("v")); + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).has(s)).toBe(true); +}); + +test("a local declared in the loop is not an accumulator", () => { + const s = update(); + expect(runningExtremumUpdates(fn([loop([decl("max"), s])])).size).toBe(0); +}); + +test("another write in the loop disqualifies the update", () => { + const s = update(); + const body = [decl("max"), loop([assign("max", num(1)), s])]; + expect(runningExtremumUpdates(fn(body)).size).toBe(0); +}); + +test("the assigned value must be the compared expression", () => { + const s = update(); + s.then = [assign("max", num(5))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("outside a loop only a captured binding qualifies", () => { + const captured = update(); + expect(runningExtremumUpdates(fn([captured], ["max"])).has(captured)).toBe(true); + expect(runningExtremumUpdates(fn([update()])).size).toBe(0); +}); + +test("an else branch disqualifies the update", () => { + const s = update(); + s.else_ = [assign("v", num(0))]; + expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0); +}); + +test("an update nested in another branch does not qualify", () => { + const s = update(); + const guard: IrStmt = { + kind: "if", + cond: compare(">", ref("v"), num(0)), + then: [s], + else_: null, + loc, + }; + expect(runningExtremumUpdates(fn([decl("max"), loop([guard])])).size).toBe(0); +}); diff --git a/packages/compiler/src/backend/llvm/running-extrema.ts b/packages/compiler/src/backend/llvm/running-extrema.ts new file mode 100644 index 000000000..1ddea603d --- /dev/null +++ b/packages/compiler/src/backend/llvm/running-extrema.ts @@ -0,0 +1,110 @@ +// Running maximum and minimum updates, `if (v > max) { max = v; ... }`. +// With the accumulator in a register LLVM turns such an update into a +// select (`maxsd`, or a compare and mask), so every element waits on the +// previous one's. The update gets rarer as a scan goes on, so a branch +// predicts well; emitStmtBody gives these `if`s branch weights that say the +// update is rarely taken, and LLVM keeps the branch. A clamp of a value read +// in the same iteration is not an accumulator and keeps its select: its +// branch would go whichever way the data goes. An update nested in another +// branch keeps its select too: kept, its branch can stop LLVM from turning +// the enclosing branch into selects, and that branch may follow the data. + +import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +type Loop = IrStmt & { kind: "for" | "while" | "doWhile" | "forOf" }; + +const RELATIONAL = new Set(["<", "<=", ">", ">="]); + +/** The `if` statements of `fn` that update a running maximum or minimum: + * no `else`, a numeric `<`, `<=`, `>` or + * `>=` between a local and another expression, and a then-block that + * assigns that expression to the local. The local must carry across + * iterations: inside a loop the `if` is a statement of the innermost loop's + * body, the local is declared outside that loop, and the loop writes it only + * in this `if`; outside any loop the `if` is a statement of the function + * body and the local is a binding the function captures and writes only in + * this `if` (a callback that a loop elsewhere calls). */ +export function runningExtremumUpdates(fn: IrFunction): Set { + const found = new Set(); + const captured = new Set((fn.captures ?? []).map((c) => c.localId)); + const loops: Loop[] = []; + const candidates: { s: IrStmt; local: string; loop: Loop | undefined }[] = []; + const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (s.kind === "if") { + const local = updatedExtremum(s); + if (local !== undefined) candidates.push({ s, local, loop: loops[loops.length - 1] }); + } + const loop = + s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; + if (loop) loops.push(s); + everyStmtChild(s, expr, stmt); + if (loop) loops.pop(); + return true; + }; + for (const s of fn.body) stmt(s); + for (const { s, local, loop } of candidates) { + const scope = loop ?? (captured.has(local) ? fn.body : undefined); + const body = loop ? loop.body : fn.body; + if (scope !== undefined && body.includes(s) && writes(scope, local) === 1) found.add(s); + } + return found; +} + +/** The local an `if` updates to a new maximum or minimum, if it is one. */ +function updatedExtremum(s: IrStmt & { kind: "if" }): string | undefined { + if (s.else_ !== null && s.else_.length > 0) return undefined; + const c = s.cond; + if (c.kind !== "bin" || !RELATIONAL.has(c.op)) return undefined; + if (c.left.type.kind !== "f64" || c.right.type.kind !== "f64") return undefined; + for (const [local, other] of [ + [c.left, c.right], + [c.right, c.left], + ]) { + if (local!.kind !== "varRef") continue; + const id = local!.localId; + const assigns = s.then.some( + (x) => x.kind === "assign" && x.localId === id && sameExpr(x.value, other!), + ); + if (assigns) return id; + } + return undefined; +} + +/** Declarations and writes of `local` under `scope`. A declaration counts + * as a write too, so a local declared inside a loop does not qualify. */ +function writes(scope: IrStmt | IrStmt[], local: string): number { + let n = 0; + const expr = (e: IrExpr): boolean => { + if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === local) n++; + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if ((s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") && s.localId === local) + n++; + else if (s.kind === "tryCatch" && s.catchLocalId === local) n++; + return everyStmtChild(s, expr, stmt); + }; + if (Array.isArray(scope)) for (const s of scope) stmt(s); + else stmt(scope); + return n; +} + +/** Structural equality of two small expressions (`v`, `a[i]`), ignoring + * source locations and types; larger expressions do not compare equal. */ +function sameExpr(a: IrExpr, b: IrExpr): boolean { + let budget = 64; + const same = (x: unknown, y: unknown): boolean => { + if (--budget < 0) return false; + if (typeof x !== "object" || x === null || typeof y !== "object" || y === null) return x === y; + if (Array.isArray(x) !== Array.isArray(y)) return false; + const keys = Object.keys(x).filter((k) => k !== "loc" && k !== "type"); + if (keys.length !== Object.keys(y).filter((k) => k !== "loc" && k !== "type").length) + return false; + return keys.every((k) => + same((x as Record)[k], (y as Record)[k]), + ); + }; + return same(a, b); +} diff --git a/packages/compiler/test/running-extrema-branches.test.ts b/packages/compiler/test/running-extrema-branches.test.ts new file mode 100644 index 000000000..e1f5a7837 --- /dev/null +++ b/packages/compiler/test/running-extrema-branches.test.ts @@ -0,0 +1,75 @@ +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-running-extrema-")); + 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" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return await readFile(outPath, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const RARE = + /br i1 %\S+, label %if\.t\d+, label %if\.j\d+, !prof !\{!"branch_weights", i32 1, i32 1000\}/; + +test("a running maximum's update is weighted as rarely taken", async () => { + const llvm = await emitLlvm(` + function argmax(a: Float64Array): number { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { max = v; at = i; } + } + return at; + } + console.log(argmax(new Float64Array([1, 3, 2]))); + `); + expect(llvm).toMatch(RARE); +}); + +test("a callback's update of a captured minimum is weighted", async () => { + const llvm = await emitLlvm(` + function least(values: number[]): number { + let min = Infinity; + values.forEach((v) => { if (min > v) min = v; }); + return min; + } + console.log(least([3, 1, 2])); + `); + expect(llvm).toMatch(RARE); +}); + +test("clamps and saturating sums keep their selects", async () => { + const llvm = await emitLlvm(` + function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; + } + function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; + } + const a = new Float64Array([0.5, 2, 0.25]); + console.log(clampSum(a, 1), saturate(a, 1)); + `); + expect(llvm).not.toContain("branch_weights"); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..6dcb4b57d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7301-running-extrema-branches.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7301-running-extrema-branches.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7301-running-extrema-branches.ts b/tests/corpus/7301-running-extrema-branches.ts new file mode 100644 index 000000000..9941ce538 --- /dev/null +++ b/tests/corpus/7301-running-extrema-branches.ts @@ -0,0 +1,169 @@ +// Running maximum and minimum scans over ascending, descending, alternating, +// pseudo-random and NaN-holding data: plain for, while, do-while and for-of +// loops, `<=` and `>=` ties, an argmax that records its index, typed array +// and array element reads, callbacks that update captured bindings, a scan +// nested in an outer loop, an update nested in another branch, and the clamp +// and saturating updates that are not running extrema. + +function shapes(n: number): [string, Float64Array][] { + const ascending = new Float64Array(n); + const descending = new Float64Array(n); + const alternating = new Float64Array(n); + const random = new Float64Array(n); + const holes = new Float64Array(n); + let x = 12345; + for (let i = 0; i < n; i++) { + ascending[i] = i; + descending[i] = n - i; + alternating[i] = i % 2 === 0 ? 0 : i; + x = (x * 1103515245 + 12345) % 2147483648; + random[i] = (x % 1000) - 500; + holes[i] = i % 7 === 3 ? NaN : (x % 97) - 48; + } + return [ + ["ascending", ascending], + ["descending", descending], + ["alternating", alternating], + ["random", random], + ["nan", holes], + ]; +} + +function argmax(a: Float64Array): string { + let max = -Infinity; + let at = -1; + for (let i = 0; i < a.length; i++) { + const v = a[i]!; + if (v > max) { + max = v; + at = i; + } + } + return `${max}@${at}`; +} + +function lastMin(a: Float64Array): string { + let min = Infinity; + let at = -1; + let i = 0; + while (i < a.length) { + if (a[i]! <= min) { + min = a[i]!; + at = i; + } + i++; + } + return `${min}@${at}`; +} + +function bothEnds(values: number[]): string { + let lo = Infinity; + let hi = -Infinity; + for (const v of values) { + if (lo > v) lo = v; + if (v >= hi) hi = v; + } + return `${lo}..${hi}`; +} + +function firstDrop(a: Float64Array): number { + let floor = a[0]!; + let i = 0; + do { + if (a[i]! < floor) floor = a[i]!; + i++; + } while (i < a.length); + return floor; +} + +function callbackPeak(a: Float64Array): string { + let max = -Infinity; + let at = -1; + a.forEach((v, i) => { + if (v > max) { + max = v; + at = i; + } + }); + return `${max}@${at}`; +} + +function arrayCallbackMin(values: number[]): number { + let min = Infinity; + values.forEach((v) => { + if (v < min) min = v; + }); + return min; +} + +function rowMaxima(a: Float64Array, width: number): string { + const out: number[] = []; + for (let row = 0; row * width < a.length; row++) { + let best = -Infinity; + for (let j = row * width; j < Math.min(a.length, (row + 1) * width); j++) { + if (a[j]! > best) best = a[j]!; + } + out.push(best); + } + return out.join(","); +} + +function longestNonNegativeRun(a: Float64Array): number { + let longest = 0; + let run = 0; + for (let i = 0; i < a.length; i++) { + if (a[i]! >= 0) { + run++; + if (run > longest) longest = run; + } else run = 0; + } + return longest; +} + +function clampSum(a: Float64Array, hi: number): number { + let s = 0; + for (let i = 0; i < a.length; i++) { + let v = a[i]!; + if (hi < v) v = hi; + s += v; + } + return s; +} + +function saturate(a: Float64Array, hi: number): number { + let v = 0; + for (let i = 0; i < a.length; i++) { + v += a[i]!; + if (hi < v) v = hi; + } + return v; +} + +for (const [name, a] of shapes(40)) { + const values = Array.from(a); + console.log( + name, + argmax(a), + lastMin(a), + bothEnds(values), + firstDrop(a), + callbackPeak(a), + arrayCallbackMin(values), + ); + console.log( + " rows", + rowMaxima(a, 7), + "run", + longestNonNegativeRun(a), + "clamp", + clampSum(a, 5), + "saturate", + saturate(a, 30), + ); +} + +const signed = new Float64Array([0, -0, 0, -0]); +let top = -Infinity; +for (let i = 0; i < signed.length; i++) if (signed[i]! >= top) top = signed[i]!; +console.log("signed zero", Object.is(top, -0), argmax(signed), lastMin(signed)); +console.log("empty", argmax(new Float64Array(0)), bothEnds([]), arrayCallbackMin([])); diff --git a/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json new file mode 100644 index 000000000..74df88f0c --- /dev/null +++ b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 5800 } From 14694925c7d2edfa5c681c2d623f39b0ce3494af Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:32:24 -0500 Subject: [PATCH 029/191] 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 030/191] 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 031/191] 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 8aac437ed281fbedde2f96296c6ca867035b3e0a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:53:50 -0500 Subject: [PATCH 032/191] Keep reduce accumulators and callback argument counts as Node runs them Array reduce typed its accumulator by the call's checked type, widened with undefined only when the callback could return a missing value or a receiver without an initial value could have holes. Three runtime values fell outside it and threw "not representable in the target union": - an initial value read from a missing element; - a callback result of another primitive (`missing[0] ?? "fallback"`); - a callback wrapped in parentheses, which the analysis and the callback lowering did not see as the arrow it wraps. The accumulator now also takes undefined from a possibly missing initial value, and the primitive arms the callback's result can have. Array callbacks are taken without their parentheses. `arguments.length` in a fixed-arity function expression folded to its declared parameter count. When the expression flows into a fixed-arity function type with more parameters, as array callbacks do (value, index, array, and the accumulator for reduce), every call passes that type's count, and the length is that count. --- .../lowering/containers/callback-arguments.ts | 2 + .../lowering/expressions/property-access.ts | 30 +++++++++++-- .../compiler/src/frontend/lowering/lowerer.ts | 32 ++++++++++---- .../7184-reduce-runtime-accumulator.ts.json | 6 +++ ...85-array-callback-arguments-length.ts.json | 6 +++ .../corpus/7184-reduce-runtime-accumulator.ts | 24 +++++++++++ .../7185-array-callback-arguments-length.ts | 42 +++++++++++++++++++ .../7184-reduce-runtime-accumulator.ts.json | 1 + ...85-array-callback-arguments-length.ts.json | 1 + 9 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json create mode 100644 tests/corpus/7184-reduce-runtime-accumulator.ts create mode 100644 tests/corpus/7185-array-callback-arguments-length.ts create mode 100644 tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json create mode 100644 tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 9db04e95b..d95640978 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -53,6 +53,8 @@ export function lowerArrayCallback( bindUntyped = false, expectedReturn?: IrType, ): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } { + // `xs.map(((x) => x))`: a parenthesized callback is the function itself. + while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression; // The ambient `Boolean` as the callback (`flags.every(Boolean)`, // `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the // ToBoolean of its FIRST argument and ignores the rest, so the callback diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7f3910c15..44d38acd4 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -666,11 +666,12 @@ export function lowerPropertyAccessExpr( ); if (fixedArity) { // A `this` parameter is type-only: callers never pass it. + const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( + (p) => !isThisParameter(p), + ).length; return { kind: "numLit", - value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( - (p) => !isThisParameter(p), - ).length, + value: Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0), type: F64, loc, }; @@ -4440,3 +4441,26 @@ function lowerPromiseConstructorRead( } return null; } + +/** The argument count every caller passes a function expression whose + * value flows into one fixed-arity function type, an array method's + * callback or an annotated slot: that type's parameter count. Null when + * there is no such type or its arity varies. */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { + if (!ts.isFunctionExpression(fn)) return null; + const contextual = lowerer.checker.getContextualType(fn); + if (contextual === undefined) return null; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + if (signatures.length !== 1) return null; + const declaration = signatures[0]!.declaration; + if (declaration === undefined) return null; + let count = 0; + for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { + if (isThisParameter(parameter)) continue; + if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null; + count++; + } + return count; +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b02a0a843..a42cadb48 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6619,7 +6619,10 @@ export class Lowerer { node.expression.name.text, node.arguments.nodes.length >= 2, ); - const callback = node.arguments.nodes[0]; + // `xs.map(((x) => x))`: the parenthesized callback is the arrow. + let callback = node.arguments.nodes[0]; + while (callback !== undefined && ts.isParenthesizedExpression(callback)) + callback = callback.expression; if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) { const receiverNode = node.expression.expression; const receiverTs = this.typeOf(receiverNode); @@ -6678,13 +6681,26 @@ export class Lowerer { presentSites.set(node, elementsPresent && !reduce); if (elementsPresent && reduce && retypeInlineCallback(callback, callbackIndices)) changed = true; - if ( - (method === "reduce" || method === "reduceRight") && - ((!elementsPresent && node.arguments.nodes.length < 2) || - callbackReturnsOptional(callback)) - ) { - if (promoteHofCallback(callback, [0, 1])) changed = true; - const result = this.runtimeOptionalType(this.irTypeOf(node)); + // The accumulator holds undefined when the callback can return + // it, when an initial value can be missing, or when elements can + // be absent and there is no initial value. It holds another + // primitive when the callback's result can be one at run time + // (`missing[0] ?? "fallback"`). + const initial = node.arguments.nodes[1]; + const optional = + (!elementsPresent && initial === undefined) || + (initial !== undefined && !ts.isSpreadElement(initial) && mayBeOptional(initial)) || + callbackReturnsOptional(callback); + const arithmetic = + reduce && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) + ? lambdaArithmeticReturn(callback) + : null; + if (reduce && (optional || arithmetic !== null)) { + if (optional && promoteHofCallback(callback, [0, 1])) changed = true; + let result = this.irTypeOf(node); + if (arithmetic !== null) + result = joinPrimitiveTypes([result, arithmetic]) ?? result; + if (optional) result = this.runtimeOptionalType(result); siteDependencies?.read(node); const previous = this.runtimeOptionalReduceTypes.get(node); if (!previous || !typeEquals(previous, result)) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..b93eb61b6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7184-reduce-runtime-accumulator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..2514bcaa8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7185-array-callback-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7185-array-callback-arguments-length.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7184-reduce-runtime-accumulator.ts b/tests/corpus/7184-reduce-runtime-accumulator.ts new file mode 100644 index 000000000..bd2c8e4d9 --- /dev/null +++ b/tests/corpus/7184-reduce-runtime-accumulator.ts @@ -0,0 +1,24 @@ +// Array reduce keeps the values its callback and initial value produce at +// run time: an initial value read from a missing element is undefined, a +// callback can produce another primitive through `??`, and a parenthesized +// callback widens like a bare one. +const noStrings: string[] = []; +const noNumbers: number[] = []; + +const fromMissingInit = ([] as number[]).reduce((_acc, _value) => "ok", noStrings[0]); +console.log(typeof fromMissingInit, fromMissingInit); + +const nonEmpty = [1, 2].reduce((acc, value) => acc + value, noNumbers[0]); +console.log(typeof nonEmpty, nonEmpty); + +const fallback = [1, 2].reduce((_acc, _value) => noNumbers[0] ?? "fallback"); +console.log(typeof fallback, fallback); + +const parenthesized = [1, 2].reduce((acc, x) => (x === 1 ? noStrings[0] : acc + "!"), ""); +console.log(parenthesized); + +const wrapped = [1, 2].reduce(((acc, x) => (x === 1 ? noStrings[0] : acc + "!")), ""); +console.log(wrapped); + +const right = ["a", "b"].reduceRight((acc, s) => acc + s, noStrings[0]); +console.log(right); diff --git a/tests/corpus/7185-array-callback-arguments-length.ts b/tests/corpus/7185-array-callback-arguments-length.ts new file mode 100644 index 000000000..ae5a8d847 --- /dev/null +++ b/tests/corpus/7185-array-callback-arguments-length.ts @@ -0,0 +1,42 @@ +// `arguments.length` in a function-expression callback counts every +// argument the array method passes: value, index and array, plus the +// accumulator for reduce. One stored in a fixed-arity function type counts +// that type's parameters, which every call passes. +const seen: number[] = []; +[10, 20].forEach(function (value) { + seen.push(arguments.length, value); +}); +console.log(seen.join(",")); + +console.log( + [1, 2] + .map(function (value) { + return `${value}:${arguments.length}`; + }) + .join(" "), +); + +console.log([5].filter(function () { + return arguments.length === 3; +}).length); + +console.log([1].reduce(function (acc, value) { + return acc + value + arguments.length; +}, 0)); + +console.log(["x", "y"].reduceRight(function (acc: string, value: string) { + return `${acc}${value}${arguments.length}`; +}, "")); + +console.log([3, 4].some(function (value) { + return arguments.length === 3 && value === 4; +})); + +const sum: (a: number, b: number) => number = function (a) { + return a + arguments.length; +}; +console.log(sum(1, 2)); + +console.log([3, 1, 2].sort(function (a, b) { + return arguments.length === 2 ? a - b : 0; +}).join(",")); diff --git a/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json new file mode 100644 index 000000000..c480bea3f --- /dev/null +++ b/tests/harness/corpus-costs/7184-reduce-runtime-accumulator.ts.json @@ -0,0 +1 @@ +{ "plain": 2541, "san": 3260 } diff --git a/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json new file mode 100644 index 000000000..b86a9e70a --- /dev/null +++ b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 2528, "san": 3201 } From 17b70e6d6a02ca09bdb6f9214e73305731a03410 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:57:59 -0500 Subject: [PATCH 033/191] 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 20f4d1ff125804302495c2f357f1f10cfa5efa3d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:08:35 -0500 Subject: [PATCH 034/191] Support the legacy createCipheriv modes and crypto.checkPrime/generatePrime --- .../limitations/builtin-apis/crypto.mdx | 5 +- .../src/generated/node-v24-compatibility.json | 24 +- .../generated/node-v24-internal.json | 40 +- internal/compatibility/static-support.json | 8 +- .../ambient/scriptc-node-fallback.d.ts | 58 +- .../compiler/src/backend/llvm/lib-shared.ts | 6 + .../src/backend/target-diagnostics.ts | 8 +- .../src/frontend/lowering/builtins/crypto.ts | 77 ++- .../src/frontend/lowering/surfaces.ts | 9 +- packages/compiler/src/ir/builtin-effects.ts | 6 + .../compiler/src/ir/builtin-signatures.ts | 10 + packages/compiler/src/ir/runtime-features.ts | 18 +- packages/compiler/src/library/ir-policy.ts | 2 + packages/compiler/surface-manifest.json | 26 +- .../corpus/7150-cipher-legacy-modes.ts.json | 6 + .../corpus/7151-cipher-legacy-errors.cjs.json | 6 + .../tests/corpus/7152-crypto-prime.ts.json | 6 + .../corpus/7153-crypto-prime-errors.cjs.json | 6 + packages/runtime/src/scr_cipher.c | 324 +++++++++-- packages/runtime/src/scr_crypto.h | 8 + packages/runtime/src/scr_dh.c | 536 +++++++++++++++++- tests/corpus/7150-cipher-legacy-modes.ts | 74 +++ tests/corpus/7151-cipher-legacy-errors.cjs | 83 +++ tests/corpus/7152-crypto-prime.ts | 96 ++++ tests/corpus/7153-crypto-prime-errors.cjs | 80 +++ tests/fixtures/node-types/fenced.ts | 8 +- .../__snapshots__/node-types-fenced.txt | 14 +- .../7150-cipher-legacy-modes.ts.json | 1 + .../7151-cipher-legacy-errors.cjs.json | 1 + .../corpus-costs/7152-crypto-prime.ts.json | 1 + .../7153-crypto-prime-errors.cjs.json | 1 + tests/harness/errors.test.ts | 5 +- 32 files changed, 1445 insertions(+), 108 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json create mode 100644 tests/corpus/7150-cipher-legacy-modes.ts create mode 100644 tests/corpus/7151-cipher-legacy-errors.cjs create mode 100644 tests/corpus/7152-crypto-prime.ts create mode 100644 tests/corpus/7153-crypto-prime-errors.cjs create mode 100644 tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json create mode 100644 tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json create mode 100644 tests/harness/corpus-costs/7152-crypto-prime.ts.json create mode 100644 tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index c84ce3e52..971065408 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,7 @@ `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, and GCM modes 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, 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. 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. @@ -25,6 +25,9 @@ `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. + Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. + Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. 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. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 574c49ff4..c341da852 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11085,9 +11085,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11108,9 +11108,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11614,9 +11614,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -11637,9 +11637,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "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 31ce5da02..9393fcaa8 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19328,6 +19328,10 @@ "tests/corpus/7084-crypto-key-pairs.cjs", "tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs", + "tests/corpus/7150-cipher-legacy-modes.ts", + "tests/corpus/7151-cipher-legacy-errors.cjs", + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19658,8 +19662,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19681,8 +19689,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.checkPrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -19707,6 +19719,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "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", @@ -19746,6 +19760,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "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", @@ -20364,8 +20380,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrime", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, @@ -20387,8 +20407,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.generatePrimeSync", + "tests": [ + "tests/corpus/7152-crypto-prime.ts", + "tests/corpus/7153-crypto-prime-errors.cjs" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index a59d6365a..06aad26cf 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -42,8 +42,10 @@ "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], "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.createCipheriv": ["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/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.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.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"], @@ -120,6 +122,8 @@ "node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"], "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.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 2a3f11be9..519608393 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3171,7 +3171,8 @@ 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, and gcm modes and + /* 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(). */ export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; @@ -3351,6 +3352,61 @@ declare module "crypto" { ): DiffieHellman; export function getDiffieHellman(groupName: string): DiffieHellmanGroup; export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; + export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint; + export interface GeneratePrimeOptions { + add?: LargeNumberLike | undefined; + rem?: LargeNumberLike | undefined; + safe?: boolean | undefined; + bigint?: boolean | undefined; + } + export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions { + bigint: true; + } + export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions { + bigint?: false | undefined; + } + export function generatePrime( + size: number, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsBigInt, + callback: (err: Error | null, prime: bigint) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + callback: (err: Error | null, prime: ArrayBuffer) => void, + ): void; + export function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: (err: Error | null, prime: ArrayBuffer | bigint) => void, + ): void; + export function generatePrimeSync(size: number): ArrayBuffer; + export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptionsArrayBuffer, + ): ArrayBuffer; + export function generatePrimeSync( + size: number, + options: GeneratePrimeOptions, + ): ArrayBuffer | bigint; + export interface CheckPrimeOptions { + checks?: number | undefined; + } + export function checkPrime( + value: LargeNumberLike, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrime( + value: LargeNumberLike, + options: CheckPrimeOptions, + callback: (err: Error | null, result: boolean) => void, + ): void; + export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean; export interface ScryptOptions { N?: number; r?: number; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..23f0a0291 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -579,6 +579,10 @@ export const LIB_FN_SYMS: Record = { "crypto.ecdhNew": "scr_crypto_ecdh_new", "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", "crypto.dhNew": "scr_crypto_dh_new", + "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.checkPrimeCb": "scr_crypto_check_prime_async", + "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", + "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", "crypto.dhGroupNew": "scr_crypto_dh_group_new", "crypto.hmacNewStr": "scr_crypto_hmac_new_str", "crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes", @@ -1600,6 +1604,8 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "crypto.generateKeyPairCb", + "crypto.checkPrimeCb", + "crypto.generatePrimeCb", "process.emitWarning", // Constructor-function streams defer their events on the stream ticks. "dyn.builtinStreamClass", diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index 3e97d0c10..236ce12cf 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -3,6 +3,7 @@ import type { LlvmUnsupportedError } from "./llvm/emitter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { type IrModule, type SrcLoc } from "../ir/ir.js"; import { + DH_LIB_FNS, PKI_LIB_FNS, moduleUsesFetch, moduleUsesWebCryptoAsymmetric, @@ -232,9 +233,12 @@ export function moduleWasiUnavailableSurface( }; return false; } - if (node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew") { + if (DH_LIB_FNS.has(node.fn)) { found = { - surface: "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)", + surface: + node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew" + ? "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)" + : "checkPrime and generatePrime (the wasm32-wasi runtime does not include the mbedTLS library)", loc, }; return false; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 7a1791197..47e66f9b2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -702,6 +702,8 @@ export function lowerCryptoModuleCall( } const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc); if (pki) return pki; + const prime = lowerPrimeCall(lowerer, expr, bi.member, args, loc); + if (prime) return prime; if (bi.member === "createSecretKey") { if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) lowerer.noLowering( @@ -867,11 +869,29 @@ 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}`), + ), + "des-ede3-cbc", + "des3", + "des-ede3", + "des-ede3-ecb", + "des-ede3-cfb", + "des-ede3-cfb8", + "des-ede3-cfb1", + "des-ede3-ofb", + "des-ede-cbc", + "des-ede", + "des-ede-ecb", + "des-ede-cfb", + "des-ede-ofb", + "chacha20", ]); const SUPPORTED_CIPHERS_HINT = - "the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, " + - "aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305"; + "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"; /** A literal cipher name that Node knows but the runtime does not * implement is refused at compile time. Unknown literal names keep Node's @@ -1346,6 +1366,59 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): return false; } +/** Node calls checkPrime's and generatePrime's callbacks with undefined as + * the error, where @types/node declares Error | null: whether this + * callback's error parameter can hold undefined. One that cannot receives + * null instead. */ +function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean { + const type = lowerer.irTypeOf(callback); + if (type.kind !== "func") return true; + const error = type.params[0]; + if (error === undefined || error.kind === "dyn" || error.kind === "undefinedT") return true; + if (error.kind !== "union") return false; + return !!lowerer.unions.get(error.unionId)?.arms.some((arm) => arm.kind === "undefinedT"); +} + +/** 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. */ +function lowerPrimeCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr | null { + const forms: Readonly> = { + 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])"], + generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], + }; + const form = forms[member]; + if (!form) return null; + const [fn, min, max, supported] = form; + if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) + lowerer.noLowering( + `crypto.${member} with ${args.length} arguments`, + expr, + `${supported} is supported`, + ); + const values = Array.from({ length: max }, (_, i) => dynArg(lowerer, args[i], loc)); + if (fn === "crypto.checkPrimeCb" || fn === "crypto.generatePrimeCb") { + const callback = args[args.length - 1]; + const nullError = callback !== undefined && !takesUndefinedError(lowerer, callback); + values.push({ kind: "boolLit", value: nullError, type: BOOL, loc }); + return { kind: "libCall", fn, args: values, type: VOID, loc }; + } + const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; + const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (result === null || result.kind === "dyn") return call; + return lowerer.coerceInto(expr, call, result); +} + /** The asymmetric-key and signature members (scr_pki.c): checked-dynamic * arguments, and the result converted to the call site's type * (KeyObject, Sign and Verify are checked-dynamic handles; key pairs, diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 0167adf12..cb16fd257 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1590,6 +1590,12 @@ export const BUILTIN_MODULE_FNS: Record< createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN }, getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN }, + // Prime checks and generation (lowerPrimeCall): checked-dynamic + // arguments, results converted to the call site's type. + checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, + generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, + generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, }, zlib: { // The default-options convenience family is special-cased in @@ -3099,7 +3105,8 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< ["getCipherInfo"].map((m) => [ m, "cipher metadata has no lowering — createCipheriv and createDecipheriv support " + - "aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305", + "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: diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..e98ae0bd9 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,12 @@ 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 candidate, size, options, add and rem errors, and OpenSSL's + // generation errors. + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", // node:test's { timeout } and { concurrency } range checks. "test.register", "test.registerEmpty", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..edadaa244 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2820,6 +2820,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a * native DiffieHellmanGroup handle. */ "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, + /** checkPrimeSync(candidate, options) and generatePrimeSync(size, + * options) over checked-dynamic arguments (scr_dh.c): a boolean, and an + * ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take + * the callback and whether its error parameter takes null for Node's + * undefined, and call it in a later macrotask; Node's argument errors + * throw (may-throw). */ + "crypto.checkPrimeSync": { 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 }, "crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T }, "crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T }, "crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5b984f318..ec2edb931 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -626,6 +626,16 @@ const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r; /* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */ const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."]; +/** The scr_dh.c entry points (the dh unit). */ +export const DH_LIB_FNS: ReadonlySet = new Set([ + "crypto.dhNew", + "crypto.dhGroupNew", + "crypto.checkPrimeSync", + "crypto.checkPrimeCb", + "crypto.generatePrimeSync", + "crypto.generatePrimeCb", +]); + /** The scr_pki.c entry points (the pki unit). */ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.createPrivateKey", @@ -820,9 +830,10 @@ const RUNTIME_FEATURE_RULES = { * process-warning unit, and unknown encodings and formats render through * the inspect unit. */ ecdh: rule({ fns: ["crypto.ecdhNew", "crypto.ecdhConvertKey"] }), - /** scr_dh.c over the mbedTLS archive; unknown encodings render through + /** scr_dh.c over the mbedTLS archive, also for checkPrime and + * generatePrime; unknown encodings and argument values render through * the inspect unit. */ - dh: rule({ fns: ["crypto.dhNew", "crypto.dhGroupNew"] }), + dh: rule({ fns: [...DH_LIB_FNS] }), emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), @@ -872,8 +883,7 @@ const RUNTIME_FEATURE_RULES = { "webcrypto.unwrapKey", "crypto.ecdhNew", "crypto.ecdhConvertKey", - "crypto.dhNew", - "crypto.dhGroupNew", + ...DH_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ], diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index fb0575121..e8fad6dd1 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -54,6 +54,8 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"], ["crypto.dhNew", "crypto.createDiffieHellman"], ["crypto.dhGroupNew", "crypto.getDiffieHellman and createDiffieHellmanGroup"], + ["crypto.checkPrime", "crypto.checkPrime and checkPrimeSync"], + ["crypto.generatePrime", "crypto.generatePrime and generatePrimeSync"], ["crypto.createPrivateKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.createPublicKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.createSign", "asymmetric KeyObjects, generateKeyPair, sign and verify"], diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 45b3edbaa..d6eb69f6f 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -480,6 +480,18 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.crypto.checkPrime", + "kind": "node-builtin", + "name": "crypto.checkPrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.checkPrimeSync", + "kind": "node-builtin", + "name": "crypto.checkPrimeSync", + "status": "static" + }, { "id": "node-builtin.crypto.createCipheriv", "kind": "node-builtin", @@ -584,13 +596,25 @@ "name": "crypto.generateKeySync", "status": "static" }, + { + "id": "node-builtin.crypto.generatePrime", + "kind": "node-builtin", + "name": "crypto.generatePrime", + "status": "static" + }, + { + "id": "node-builtin.crypto.generatePrimeSync", + "kind": "node-builtin", + "name": "crypto.generatePrimeSync", + "status": "static" + }, { "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, and gcm modes and chacha20-poly1305" + "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" }, { "id": "node-builtin.crypto.getDiffieHellman", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..c0e5f9812 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7150-cipher-legacy-modes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7150-cipher-legacy-modes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..1f37c6b64 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7151-cipher-legacy-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json new file mode 100644 index 000000000..722ebaacb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7152-crypto-prime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7152-crypto-prime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..9d121895a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7153-crypto-prime-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7153-crypto-prime-errors.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index c8a10a731..00d229bad 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -20,16 +20,30 @@ #include "mbedtls/aes.h" #include "mbedtls/chacha20.h" +#include "mbedtls/des.h" #include "mbedtls/gcm.h" #include "mbedtls/poly1305.h" #include "mbedtls/platform_util.h" -typedef enum { SC_CBC, SC_CTR, SC_GCM, SC_CHACHA } ScCipherMode; +/* SC_CHACHA is chacha20-poly1305; SC_CHACHA20 the bare stream cipher. */ +typedef enum { + SC_CBC, + SC_CTR, + SC_GCM, + SC_CHACHA, + SC_ECB, + SC_CFB, + SC_CFB8, + SC_CFB1, + SC_OFB, + SC_CHACHA20, +} ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; + bool des; /* triple DES (two- or three-key) rather than AES */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. @@ -41,6 +55,35 @@ static const ScCipherSpec sc_specs[] = { {"aes-128-gcm", SC_GCM, 16}, {"aes-192-gcm", SC_GCM, 24}, {"aes-256-gcm", SC_GCM, 32}, {"id-aes128-gcm", SC_GCM, 16}, {"id-aes192-gcm", SC_GCM, 24}, {"id-aes256-gcm", SC_GCM, 32}, {"chacha20-poly1305", SC_CHACHA, 32}, + {"aes-128-ecb", SC_ECB, 16}, {"aes-192-ecb", SC_ECB, 24}, {"aes-256-ecb", SC_ECB, 32}, + {"aes-128-cfb", SC_CFB, 16}, {"aes-192-cfb", SC_CFB, 24}, {"aes-256-cfb", SC_CFB, 32}, + {"aes-128-cfb8", SC_CFB8, 16}, {"aes-192-cfb8", SC_CFB8, 24}, {"aes-256-cfb8", SC_CFB8, 32}, + {"aes-128-cfb1", SC_CFB1, 16}, {"aes-192-cfb1", SC_CFB1, 24}, {"aes-256-cfb1", SC_CFB1, 32}, + {"aes-128-ofb", SC_OFB, 16}, {"aes-192-ofb", SC_OFB, 24}, {"aes-256-ofb", SC_OFB, 32}, + {"des-ede3-cbc", SC_CBC, 24, true}, {"des3", SC_CBC, 24, true}, + {"des-ede3", SC_ECB, 24, true}, {"des-ede3-ecb", SC_ECB, 24, true}, + {"des-ede3-cfb", SC_CFB, 24, true}, {"des-ede3-cfb8", SC_CFB8, 24, true}, + {"des-ede3-cfb1", SC_CFB1, 24, true}, {"des-ede3-ofb", SC_OFB, 24, true}, + {"des-ede-cbc", SC_CBC, 16, true}, {"des-ede", SC_ECB, 16, true}, + {"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}, +}; + +/* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not + * load: crypto.getCipherInfo() knows them, and createCipheriv checks their + * IV length, then fails with OpenSSL's "unsupported" error. */ +static const struct { + const char *name; + unsigned iv_len; +} sc_legacy[] = { + {"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, + {"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, + {"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, + {"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, + {"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, + {"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, + {"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8}, }; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that @@ -88,15 +131,21 @@ typedef struct ScCipher { ScCipherMode mode; bool decrypt; bool active; /* false after final() or a failed backend call */ - bool pad; /* CBC automatic padding */ + bool pad; /* CBC and ECB automatic padding */ + bool des; /* triple DES rather than AES */ + unsigned bs; /* the block size: 16 for AES, 8 for triple DES */ bool data_started; /* a non-empty update() ran, so non-empty AAD is refused */ int tag_len; /* SC_NO_TAG until known */ int tag_state; unsigned char tag[16]; - unsigned char block[16]; /* CBC: chaining value; CTR: counter block */ - unsigned char buf[16]; /* CBC: buffered input; CTR: keystream block */ - size_t bufsz; /* CBC: buffered bytes; CTR: keystream offset */ + unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: + shift register; chacha20: the IV */ + unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ + size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream + offset */ + uint64_t chacha_left; /* chacha20: bytes before the low counter word wraps */ mbedtls_aes_context aes; + mbedtls_des3_context des3; mbedtls_gcm_context gcm; mbedtls_chacha20_context chacha; mbedtls_poly1305_context poly; @@ -127,6 +176,7 @@ static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { static void sc_gcfree(void *h) { ScCipher *c = h; mbedtls_aes_free(&c->aes); + mbedtls_des3_free(&c->des3); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); @@ -184,18 +234,21 @@ static void sc_invalid_tag(unsigned long length) { /* An OpenSSL provider error as Node reports it: the formatted message plus * own opensslErrorStack, library, reason and code properties, in Node's * key order. */ -static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { +/* OpenSSL's error with its library, reason and error stack; Node's + * code. */ +static void sc_throw_openssl_in(const char *packed, const char *library, const char *reason, + const char *code) { char message[128]; - snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason); + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); scr_error_set_prop(e, "opensslErrorStack", 17, stack); - ScrStr *library = scr_str_new("Provider routines", 17); - scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); - scr_str_release(library); + ScrStr *library_text = scr_str_new(library, strlen(library)); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library_text)); + scr_str_release(library_text); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); @@ -206,6 +259,10 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } +static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { + sc_throw_openssl_in(packed, "Provider routines", reason, code); +} + /* ERR_INVALID_ARG_VALUE's property flavor: "The property 'options.x' is * invalid. Received ". */ static void sc_invalid_property(const char *name, const ScrDyn *value) { @@ -440,6 +497,27 @@ static const ScCipherSpec *sc_lookup(const ScrStr *name) { return NULL; } +/* The legacy-provider IV length of `name`, or -1 for other names. */ +static int sc_legacy_iv(const ScrStr *name) { + for (size_t i = 0; i < sizeof sc_legacy / sizeof sc_legacy[0]; i++) + if (sc_ascii_eq_ci(name->data, name->len, sc_legacy[i].name)) return (int)sc_legacy[i].iv_len; + return -1; +} + +/* The IV length a mode takes; SC_GCM takes any nonzero length. */ +static size_t sc_iv_length(ScCipherMode mode, bool des) { + switch (mode) { + case SC_ECB: + return 0; + case SC_CHACHA: + return 12; + case SC_CHACHA20: + return 16; + default: + return des ? 8 : 16; + } +} + static bool sc_known_name(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_known / sizeof sc_known[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_known[i])) return true; @@ -451,9 +529,10 @@ static void sc_unsupported_cipher(const ScrStr *name) { scr_jb_init(&b); 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-cbc, aes-192-cbc, " - "aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, " - "aes-256-gcm and chacha20-poly1305"); + 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"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -522,14 +601,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } const ScCipherSpec *spec = sc_lookup(cipher->v.str); + size_t iv_len = iv_bytes ? iv_bytes->len : 0; if (!spec) { - if (sc_known_name(cipher->v.str)) sc_unsupported_cipher(cipher->v.str); - else sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + int legacy_iv = sc_legacy_iv(cipher->v.str); + if (legacy_iv >= 0) { + if (iv_len != (size_t)legacy_iv || (legacy_iv > 0 && !iv_bytes)) + sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); + else + sc_throw_openssl_in("0308010C", "digital envelope routines", "unsupported", + "ERR_OSSL_EVP_UNSUPPORTED"); + } else if (sc_known_name(cipher->v.str)) { + sc_unsupported_cipher(cipher->v.str); + } else { + sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); + } goto done; } - size_t iv_len = iv_bytes ? iv_bytes->len : 0; - bool aead = spec->mode == SC_GCM || spec->mode == SC_CHACHA; - if (iv_len == 0 || (!aead && iv_len != 16) || (spec->mode == SC_CHACHA && iv_len != 12)) { + 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)) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -570,8 +659,11 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * c->decrypt = decrypt; c->active = true; c->pad = true; + c->des = spec->des; + c->bs = spec->des ? 8 : 16; c->tag_len = tag_len; mbedtls_aes_init(&c->aes); + mbedtls_des3_init(&c->des3); mbedtls_gcm_init(&c->gcm); mbedtls_chacha20_init(&c->chacha); mbedtls_poly1305_init(&c->poly); @@ -579,9 +671,39 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * unsigned bits = spec->key_len * 8; switch (spec->mode) { case SC_CBC: - rc = decrypt ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) - : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + case SC_ECB: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: { + /* CBC and ECB decrypt with the inverse cipher; the feedback modes run + * the forward cipher both ways. */ + bool inverse = decrypt && (spec->mode == SC_CBC || spec->mode == SC_ECB); + if (spec->des && spec->key_len == 24) + rc = inverse ? mbedtls_des3_set3key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set3key_enc(&c->des3, key_bytes->data); + else if (spec->des) + rc = inverse ? mbedtls_des3_set2key_dec(&c->des3, key_bytes->data) + : mbedtls_des3_set2key_enc(&c->des3, key_bytes->data); + else + rc = inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + if (expected_iv) memcpy(c->block, iv_bytes->data, expected_iv); + break; + } + case SC_CHACHA20: + /* OpenSSL's IV is a 64-bit little-endian block counter and an 8-byte + * nonce; mbedTLS takes a 32-bit counter and a 12-byte nonce, so the + * stream restarts each time the low counter word wraps. */ memcpy(c->block, iv_bytes->data, 16); + rc = mbedtls_chacha20_setkey(&c->chacha, key_bytes->data); + if (rc == 0) { + uint32_t low = (uint32_t)c->block[0] | (uint32_t)c->block[1] << 8 | + (uint32_t)c->block[2] << 16 | (uint32_t)c->block[3] << 24; + c->chacha_left = ((uint64_t)1 << 32) - low; + c->chacha_left *= 64; + rc = mbedtls_chacha20_starts(&c->chacha, c->block + 4, low); + } break; case SC_CTR: rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); @@ -635,39 +757,123 @@ done: /* ── the cipher transforms ────────────────────────────────────────────── */ -/* Raw CBC over whole blocks. */ +/* Raw CBC or ECB over whole blocks. */ static bool sc_cbc_blocks(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + if (c->mode == SC_ECB) { + for (size_t off = 0; off < len; off += c->bs) { + int rc = c->des ? mbedtls_des3_crypt_ecb(&c->des3, in + off, out + off) + : mbedtls_aes_crypt_ecb(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, + in + off, out + off); + if (rc != 0) return false; + } + return true; + } + if (c->des) + return mbedtls_des3_crypt_cbc(&c->des3, c->decrypt ? MBEDTLS_DES_DECRYPT : MBEDTLS_DES_ENCRYPT, len, c->block, + in, out) == 0; return mbedtls_aes_crypt_cbc(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, len, c->block, in, out) == 0; } -/* OpenSSL's ossl_cipher_generic_block_update for CBC: complete blocks - * are processed, the remainder is buffered, and a decryption that ends on - * a block boundary holds the last block back for padding removal. Returns - * the output length, or -1 when the backend fails; out has room for - * len + 16 bytes. */ +/* One forward block of the feedback modes' cipher. */ +static bool sc_forward(ScCipher *c, const unsigned char *in, unsigned char *out) { + return (c->des ? mbedtls_des3_crypt_ecb(&c->des3, in, out) + : mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out)) == 0; +} + +/* CFB (full-block feedback), CFB8, CFB1 and OFB, as OpenSSL's + * CRYPTO_cfb128_encrypt, CRYPTO_cfb128_8_encrypt, CRYPTO_cfb128_1_encrypt + * and CRYPTO_ofb128_encrypt: CFB and OFB keep their keystream offset across + * updates; CFB1 feeds back one bit at a time, most significant first. */ +static bool sc_feedback(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + unsigned char stream[16]; + size_t bs = c->bs; + for (size_t i = 0; i < len; i++) { + switch (c->mode) { + case SC_CFB: + case SC_OFB: + if (c->bufsz == 0 && !sc_forward(c, c->block, c->block)) return false; + out[i] = (unsigned char)(in[i] ^ c->block[c->bufsz]); + if (c->mode == SC_CFB) c->block[c->bufsz] = c->decrypt ? in[i] : out[i]; + c->bufsz = (c->bufsz + 1) % bs; + break; + case SC_CFB8: { + if (!sc_forward(c, c->block, stream)) return false; + unsigned char byte = (unsigned char)(in[i] ^ stream[0]); + memmove(c->block, c->block + 1, bs - 1); + c->block[bs - 1] = c->decrypt ? in[i] : byte; + out[i] = byte; + break; + } + default: { + unsigned char result = 0; + for (int bit = 7; bit >= 0; bit--) { + if (!sc_forward(c, c->block, stream)) return false; + unsigned in_bit = (in[i] >> bit) & 1u; + unsigned out_bit = in_bit ^ (stream[0] >> 7); + unsigned feed = c->decrypt ? in_bit : out_bit; + for (size_t k = 0; k + 1 < bs; k++) + c->block[k] = (unsigned char)(c->block[k] << 1 | c->block[k + 1] >> 7); + c->block[bs - 1] = (unsigned char)(c->block[bs - 1] << 1 | feed); + result |= (unsigned char)(out_bit << bit); + } + out[i] = result; + break; + } + } + } + mbedtls_platform_zeroize(stream, sizeof stream); + return true; +} + +/* chacha20 over len bytes: when the 64-bit counter's low word wraps, the + * high word steps (and wraps itself, as OpenSSL's does) and the stream + * restarts from block 0 of the next word. */ +static bool sc_chacha20_update(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { + while (len > 0) { + size_t n = len < c->chacha_left ? len : (size_t)c->chacha_left; + if (mbedtls_chacha20_update(&c->chacha, n, in, out) != 0) return false; + in += n; + out += n; + len -= n; + c->chacha_left -= n; + if (c->chacha_left == 0) { + for (int k = 4; k < 8 && ++c->block[k] == 0; k++) { + } + memset(c->block, 0, 4); + c->chacha_left = (uint64_t)1 << 38; + if (mbedtls_chacha20_starts(&c->chacha, c->block + 4, 0) != 0) return false; + } + } + return true; +} + +/* OpenSSL's ossl_cipher_generic_block_update for CBC and ECB: complete + * blocks are processed, the remainder is buffered, and a decryption that + * ends on a block boundary holds the last block back for padding removal. + * Returns the output length, or -1 when the backend fails; out has room + * for len + 16 bytes. */ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsigned char *out) { + size_t bs = c->bs; size_t outl = 0; size_t nextblocks; if (c->bufsz != 0) { - size_t remain = 16 - c->bufsz; + size_t remain = bs - c->bufsz; if (inl < remain) remain = inl; memcpy(c->buf + c->bufsz, in, remain); in += remain; inl -= remain; c->bufsz += remain; - nextblocks = inl & ~(size_t)15; - } else { - nextblocks = inl & ~(size_t)15; } - if (c->bufsz == 16 && (!c->decrypt || inl > 0 || !c->pad)) { - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -1; + nextblocks = inl - inl % bs; + if (c->bufsz == bs && (!c->decrypt || inl > 0 || !c->pad)) { + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -1; c->bufsz = 0; - outl = 16; - out += 16; + outl = bs; + out += bs; } if (nextblocks > 0) { - if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= 16; + if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= bs; if (nextblocks > 0) { if (!sc_cbc_blocks(c, in, nextblocks, out)) return -1; outl += nextblocks; @@ -682,33 +888,35 @@ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsi return (long)outl; } -/* ossl_cipher_generic_block_final for CBC. Returns the output length, -1 - * after throwing OpenSSL's error, or -2 when the backend fails. */ +/* ossl_cipher_generic_block_final for CBC and ECB. Returns the output + * length, -1 after throwing OpenSSL's error, or -2 when the backend + * fails. */ static long sc_cbc_final(ScCipher *c, unsigned char out[16]) { + size_t bs = c->bs; if (!c->decrypt) { if (c->pad) { - unsigned char fill = (unsigned char)(16 - c->bufsz); - for (size_t i = c->bufsz; i < 16; i++) c->buf[i] = fill; + unsigned char fill = (unsigned char)(bs - c->bufsz); + for (size_t i = c->bufsz; i < bs; i++) c->buf[i] = fill; } else if (c->bufsz == 0) { return 0; - } else if (c->bufsz != 16) { + } else if (c->bufsz != bs) { sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, out)) return -2; + if (!sc_cbc_blocks(c, c->buf, bs, out)) return -2; c->bufsz = 0; - return 16; + return (long)bs; } - if (c->bufsz != 16) { + if (c->bufsz != bs) { if (c->bufsz == 0 && !c->pad) return 0; sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } - if (!sc_cbc_blocks(c, c->buf, 16, c->buf)) return -2; - size_t len = 16; + if (!sc_cbc_blocks(c, c->buf, bs, c->buf)) return -2; + size_t len = bs; if (c->pad) { - unsigned char fill = c->buf[15]; - bool bad = fill == 0 || fill > 16; + unsigned char fill = c->buf[bs - 1]; + bool bad = fill == 0 || fill > bs; for (size_t i = 0; !bad && i < fill; i++) bad = c->buf[--len] != fill; if (bad) { sc_throw_openssl("1C800064", "bad decrypt", "ERR_OSSL_BAD_DECRYPT"); @@ -769,7 +977,8 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) size_t produced = 0; bool ok = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long n = sc_cbc_update(c, in, len, out->data); ok = n >= 0; produced = ok ? (size_t)n : 0; @@ -786,6 +995,17 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha_update(c, in, len, out->data); produced = len; break; + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + ok = sc_feedback(c, in, len, out->data); + produced = len; + break; + case SC_CHACHA20: + ok = len == 0 || sc_chacha20_update(c, in, len, out->data); + produced = len; + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -926,7 +1146,8 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { out->len = 0; bool ok = true, backend = true; switch (c->mode) { - case SC_CBC: { + case SC_CBC: + case SC_ECB: { long produced = sc_cbc_final(c, out->data); if (produced == -1) { scr_bytes_release(out); @@ -937,6 +1158,11 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { break; } case SC_CTR: + case SC_CFB: + case SC_CFB8: + case SC_CFB1: + case SC_OFB: + case SC_CHACHA20: break; case SC_GCM: { unsigned char full[16]; diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 546e3d75b..0e6c6e07f 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -108,6 +108,14 @@ 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.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 + * the error, as Node passes it, or null when `null_error`. */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options); +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error); +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options); +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error); ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt, double iterations, double keylen, ScrStr *digest); ScrBytes *scr_crypto_pbkdf2_job(ScrBytes *password, ScrBytes *salt, double iterations, diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c index cf44338f5..dfa6fd2f3 100644 --- a/packages/runtime/src/scr_dh.c +++ b/packages/runtime/src/scr_dh.c @@ -183,17 +183,17 @@ static void sd_value_too_long(void) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); } -/* validateInt32(value, name). */ -static bool sd_validate_int32(const char *name, double value) { +/* validateInt32(value, name, min). */ +static bool sd_validate_int32_min(const char *name, double value, int32_t min) { bool integer = isfinite(value) && trunc(value) == value; - if (integer && value >= INT32_MIN && value <= INT32_MAX) return true; + if (integer && value >= min && value <= INT32_MAX) return true; char received[48]; scr_num_received(value, received); char text[192]; int n = integer ? snprintf(text, sizeof text, - "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. " + "The value of \"%s\" is out of range. It must be >= %ld && <= 2147483647. " "Received %s", - name, received) + name, (long)min, received) : snprintf(text, sizeof text, "The value of \"%s\" is out of range. It must be an integer. Received %s", name, received); @@ -201,6 +201,11 @@ static bool sd_validate_int32(const char *name, double value) { return false; } +/* validateInt32(value, name). */ +static bool sd_validate_int32(const char *name, double value) { + return sd_validate_int32_min(name, value, INT32_MIN); +} + /* ── arithmetic ───────────────────────────────────────────────────────── */ /* The exponent digit width that keeps every mbedtls_mpi_exp_mod exponent @@ -630,9 +635,9 @@ static int sd_mr_rounds(size_t bits) { return bits > 2048 ? 128 : 64; } -/* BN_check_prime: trial division by the small primes, then Miller-Rabin - * rounds with random bases. */ -static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { +/* BN_is_prime_ex(x, checks): trial division by the small primes, then + * Miller-Rabin rounds with random bases, at least OpenSSL's round count. */ +static bool sd_is_prime_checks(ScDhSmallPrimes *t, const mbedtls_mpi *x, int checks) { if (mbedtls_mpi_cmp_int(x, 2) < 0) return false; if (mbedtls_mpi_get_bit(x, 0) == 0) return mbedtls_mpi_cmp_int(x, 2) == 0; if (!sd_small_residues(t, x)) return false; @@ -643,11 +648,17 @@ static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { ScDhMillerRabin m; bool prime = sd_mr_init(&m, x); int rounds = sd_mr_rounds(mbedtls_mpi_bitlen(x)); + if (checks > rounds) rounds = checks; for (int i = 0; i < rounds && prime; i++) prime = sd_mr_round(&m, x, false) == 1; sd_mr_free(&m); return prime; } +/* BN_check_prime. */ +static bool sd_is_prime(ScDhSmallPrimes *t, const mbedtls_mpi *x) { + return sd_is_prime_checks(t, x, 0); +} + /* DH_check for parameters without q, as ncrypto's CheckDhParams reports * it. */ static int sd_check_params(const mbedtls_mpi *p, const mbedtls_mpi *g) { @@ -793,8 +804,9 @@ static void sd_sieve_window(ScDhSieve *sv, unsigned add) { * when p is a strong probable prime to the base 2 and q = (p - 1) / 2 * passes OpenSSL's Miller-Rabin round count; a prime q then proves p prime * (Pocklington's criterion with 2^(p-1) ≡ 1 and gcd(2^2 - 1, p) = 1). - * Candidates past `bits` bits take a new start. */ -static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem) { + * Candidates past `bits` bits take a new start. With `top_two`, starts + * also set the second-highest bit, as BN_RAND_TOP_TWO does. */ +static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, unsigned rem, bool top_two) { ScDhSieve *sv = sd_sieve_new(bits, add); int rounds = sd_mr_rounds(bits); mbedtls_mpi start, q; @@ -803,7 +815,8 @@ static bool sd_generate_safe_prime(mbedtls_mpi *p, size_t bits, unsigned add, un int ret = 0; while (ret == 0) { mbedtls_mpi_uint r; - if (!sd_random_bits(&start, bits, true) || mbedtls_mpi_mod_int(&r, &start, add) != 0 || + if (!sd_random_bits(&start, bits, true) || (top_two && mbedtls_mpi_set_bit(&start, bits - 2, 1) != 0) || + mbedtls_mpi_mod_int(&r, &start, add) != 0 || mbedtls_mpi_sub_int(&start, &start, (mbedtls_mpi_sint)r) != 0 || mbedtls_mpi_add_int(&start, &start, rem) != 0 || (mbedtls_mpi_bitlen(&start) < bits && mbedtls_mpi_add_int(&start, &start, add) != 0)) { @@ -900,7 +913,7 @@ static ScrDyn *sd_generate(double bits, const ScrBytes *generator_bytes, double if (generator == 2) add = 24; else if (generator == 5) add = 60; ScDh *d = sd_alloc(); - if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { + if (!sd_generate_safe_prime(&d->p, (size_t)bits, add, add - 1, false) || mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { sd_release(d); sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); return NULL; @@ -1034,6 +1047,505 @@ ScrDyn *scr_crypto_dh_group_new(ScrDyn *name) { return sd_box(d); } +/* ── checkPrime and generatePrime ───────────────────────────────────────── + * crypto.checkPrime(Sync) and generatePrime(Sync) over the primality test + * and prime search above, with random.js's argument checks and ncrypto's + * RandomPrimeConfig and BN_generate_prime_ex2 errors. The callback forms + * compute at once and call back in a later macrotask. */ + +/* The longest candidate the Miller-Rabin rounds take: sd_mont_init's R^2 + * has twice the candidate's limbs, within MBEDTLS_MPI_MAX_LIMBS, and + * mbedtls_mpi_exp_mod stops at MBEDTLS_MPI_MAX_BITS. */ +#if SD_MONT +#define SD_MAX_PRIME_BITS 262144 +#else +#define SD_MAX_PRIME_BITS MBEDTLS_MPI_MAX_BITS +#endif + +static void sd_prime_too_long(const char *what) { + char message[128]; + int n = snprintf(message, sizeof message, "%s longer than %d bits have no native implementation", what, + SD_MAX_PRIME_BITS); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, n < 0 ? 0 : (size_t)n, "SC2020"); +} + +/* An OpenSSL bignum error as a crypto job reports it: an Error with the + * formatted message and no other properties. */ +static void sd_throw_bignum(const char *packed, const char *reason) { + char message[96]; + int n = snprintf(message, sizeof message, "error:%s:bignum routines::%s", packed, reason); + ScrStr *text = scr_str_new(message, n < 0 ? 0 : (size_t)n); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* validateObject(value, name): an object that is neither null nor an + * array. */ +static bool sd_validate_object(const char *name, const ScrDyn *value) { + ScrStr *type = scr_dyn_typeof(value); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (object && value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_ARR) return true; + scr_dyn_arg_type_fail(name, "of type object", value); + return false; +} + +static bool sd_validate_function(const char *name, const ScrDyn *value) { + if (scr_dyn_is_callable(value)) return true; + scr_dyn_arg_type_fail(name, "of type function", value); + return false; +} + +/* options[key] (borrowed), or undefined. */ +static const ScrDyn *sd_option(const ScrDyn *options, const char *key) { + const ScrDyn *value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, key, strlen(key)) : NULL; + return value ? value : scr_dyn_undefined(); +} + +static const char sd_large_number_types[] = + "of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"; + +/* A LargeNumberLike argument as a bignum: a bigint (unsignedBigIntToBuffer, + * which rejects negative values), or the bytes of an ArrayBuffer or view. + * False with the error pending. */ +static bool sd_large_number(mbedtls_mpi *x, const char *name, bool property, const ScrDyn *value) { + if (value->kind == SCR_DYN_BIGINT) { + ScrStr *text = scr_bigint_to_string(value->v.bigint, 16); + if (!text) return false; + bool negative = text->len > 0 && text->data[0] == '-'; + if (negative) { + scr_str_release(text); + ScrStr *decimal = scr_bigint_to_string(value->v.bigint, 10); + if (!decimal) return false; + /* ERR_OUT_OF_RANGE's received value groups the digits of a bigint + * past 2^32 in threes. */ + ScrBigInt *limit = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(value->v.bigint, limit) < 0; + scr_bigint_release(limit); + char digits[160]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + for (size_t i = 0; i < len; i++) { + size_t rest = len - i; + if (group && i > 1 && rest % 3 == 0) digits[out++] = '_'; + digits[out++] = decimal->data[i]; + } + digits[out] = '\0'; + scr_str_release(decimal); + char message[256]; + int n = snprintf(message, sizeof message, "The value of \"%s\" is out of range. It must be >= 0. Received %sn", + name, digits); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; + } + char *hex = malloc(text->len + 1); + if (!hex) scr_trap("scriptc: out of memory\n"); + memcpy(hex, text->data, text->len); + hex[text->len] = '\0'; + scr_str_release(text); + bool ok = strlen(hex) <= SD_MAX_VALUE_BYTES * 2; + if (ok && mbedtls_mpi_read_string(x, 16, hex) != 0) scr_trap("scriptc: out of memory\n"); + free(hex); + if (!ok) sd_value_too_long(); + return ok; + } + if (!sd_is_view(value)) { + if (property) scr_dyn_prop_type_fail(name, sd_large_number_types, value); + else scr_dyn_arg_type_fail(name, sd_large_number_types, value); + return false; + } + ScrBytes *bytes = scr_bytes_buffer_source(value); + if (!bytes) return false; + bool held = sd_read(x, bytes); + scr_bytes_release(bytes); + if (!held) sd_value_too_long(); + return held; +} + +/* calc_trial_divisions: how many of the first primes (2 included) the + * search divides a candidate of `bits` bits by. */ +static size_t sd_trial_divisions(size_t bits) { + return bits <= 512 ? 64 : bits <= 1024 ? 128 : bits <= 2048 ? 384 : bits <= 4096 ? 1024 : 2048; +} + +static uint64_t sd_word(const mbedtls_mpi *x) { + if (mbedtls_mpi_bitlen(x) > 64) return UINT64_MAX; + unsigned char buf[8]; + if (mbedtls_mpi_write_binary(x, buf, 8) != 0) return UINT64_MAX; + uint64_t w = 0; + for (int i = 0; i < 8; i++) w = w << 8 | buf[i]; + return w; +} + +static bool sd_add_word(mbedtls_mpi *x, uint64_t w) { + unsigned char buf[8]; + for (int i = 7; i >= 0; i--, w >>= 8) buf[i] = (unsigned char)w; + mbedtls_mpi t; + mbedtls_mpi_init(&t); + bool ok = mbedtls_mpi_read_binary(&t, buf, 8) == 0 && mbedtls_mpi_add_mpi(x, x, &t) == 0; + mbedtls_mpi_free(&t); + return ok; +} + +/* probable_prime and bn_probable_prime_dh: a random odd candidate of + * `bits` bits (the top two bits set, or with `add` the top bit, moved to + * rem modulo add), then the first one stepping by 2, by 4 for a safe + * prime, or by add, that no trial-division prime divides, nor for a safe + * prime divides (p - 1) / 2. */ +static bool sd_probable_prime(ScDhSmallPrimes *t, mbedtls_mpi *rnd, size_t bits, bool safe, const mbedtls_mpi *add, + const mbedtls_mpi *rem) { + size_t trials = sd_trial_divisions(bits) - 1; /* odd primes */ + uint64_t step = safe ? 4 : 2; + uint64_t maxdelta = UINT64_MAX - t->primes[trials - 1]; + if (add) { + step = sd_word(add); + if (maxdelta > UINT64_MAX - step) maxdelta = UINT64_MAX - step; + } + mbedtls_mpi r; + mbedtls_mpi_init(&r); + bool ok = true; + for (;;) { + if (!sd_random_bits(rnd, bits, true)) ok = false; + if (ok && !add) { + if ((bits >= 2 && mbedtls_mpi_set_bit(rnd, bits - 2, 1) != 0) || (safe && mbedtls_mpi_set_bit(rnd, 1, 1) != 0)) + ok = false; + } else if (ok) { + ok = mbedtls_mpi_mod_mpi(&r, rnd, add) == 0 && mbedtls_mpi_sub_mpi(rnd, rnd, &r) == 0 && + (rem ? mbedtls_mpi_add_mpi(rnd, rnd, rem) == 0 : mbedtls_mpi_add_int(rnd, rnd, safe ? 3 : 1) == 0); + if (ok && (mbedtls_mpi_bitlen(rnd) < bits || sd_word(rnd) < (safe ? 5u : 3u))) + ok = mbedtls_mpi_add_mpi(rnd, rnd, add) == 0; + } + if (!ok || !sd_small_residues(t, rnd)) { + ok = false; + break; + } + uint64_t delta = 0, word = sd_word(rnd); + bool again = false; + for (size_t i = 0; i < trials && !again;) { + uint64_t prime = t->primes[i]; + /* A one-word candidate needs only the primes up to its square root. */ + if (bits <= 31 && delta <= 0x7fffffff && prime * prime > word + delta) break; + uint64_t m = (t->mods[i] + delta) % prime; + if (safe ? m <= 1 : m == 0) { + delta += step; + if (delta > maxdelta) again = true; + i = 0; + continue; + } + i++; + } + if (again) continue; + if (!sd_add_word(rnd, delta)) { + ok = false; + break; + } + if (add || mbedtls_mpi_bitlen(rnd) == bits) break; + } + mbedtls_mpi_free(&r); + return ok; +} + +/* BN_generate_prime_ex2(p, bits, safe, add, rem): false with OpenSSL's + * error pending. Long safe primes without add take the sieve over + * p ≡ 11 (mod 12), which every safe prime above 7 satisfies. */ +static bool sd_generate_prime(mbedtls_mpi *p, size_t bits, bool safe, const mbedtls_mpi *add, const mbedtls_mpi *rem) { + if (bits < 2 || (!add && safe && bits < 6 && bits != 3)) { + sd_throw_bignum("01800076", "bits too small"); + return false; + } + if (add && mbedtls_mpi_cmp_int(add, 0) == 0) { + sd_throw_bignum("01800067", "div by zero"); + return false; + } + if (!add && safe && bits >= 64) { + if (!sd_generate_safe_prime(p, bits, 12, 11, true)) scr_trap("scriptc: out of memory\n"); + return true; + } + ScDhSmallPrimes *t = sd_small_primes(); + mbedtls_mpi q; + mbedtls_mpi_init(&q); + bool found = false; + while (!found) { + if (!sd_probable_prime(t, p, bits, safe, add, rem)) scr_trap("scriptc: out of memory\n"); + if (!safe) { + found = sd_is_prime(t, p); + } else { + if (mbedtls_mpi_copy(&q, p) != 0 || mbedtls_mpi_shift_r(&q, 1) != 0) scr_trap("scriptc: out of memory\n"); + found = sd_is_prime(t, p) && sd_is_prime(t, &q); + } + } + mbedtls_mpi_free(&q); + free(t); + return true; +} + +/* checkPrime's arguments after the candidate: Node's options and checks + * rules. -1 with the error pending. */ +static int sd_prime_checks(const ScrDyn *options) { + if (!sd_validate_object("options", options)) return -1; + const ScrDyn *checks = sd_option(options, "checks"); + if (checks->kind == SCR_DYN_UNDEF) return 0; + if (checks->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.checks", "of type number", checks); + return -1; + } + if (!sd_validate_int32_min("options.checks", checks->v.num, 0)) return -1; + return (int)checks->v.num; +} + +/* The candidate's verdict as a boolean dyn (+1), or NULL with the error + * pending. */ +static ScrDyn *sd_check_prime(const mbedtls_mpi *candidate, int checks) { + if (mbedtls_mpi_bitlen(candidate) > SD_MAX_PRIME_BITS) { + sd_prime_too_long("checkPrime candidates"); + return NULL; + } + ScDhSmallPrimes *t = sd_small_primes(); + bool prime = sd_is_prime_checks(t, candidate, checks); + free(t); + return scr_dyn_new_bool(prime); +} + +/* A borrowed, unboxed view of each argument (+1 each); false after a + * throw. */ +static bool sd_unbox_all(ScrDyn **out, ScrDyn *const *args, size_t count) { + for (size_t i = 0; i < count; i++) out[i] = NULL; + for (size_t i = 0; i < count; i++) { + ScrDyn *value = args[i]; + out[i] = value->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(value) : scr_dyn_retain(value); + if (!out[i]) return false; + } + return true; +} + +static void sd_release_all(ScrDyn **values, size_t count) { + for (size_t i = 0; i < count; i++) + if (values[i]) scr_dyn_release(values[i]); +} + +/* checkPrimeSync(candidate[, options]). */ +ScrDyn *scr_crypto_check_prime_sync(ScrDyn *candidate, ScrDyn *options) { + ScrDyn *args[2] = {candidate, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 2) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + int checks = sd_prime_checks(empty ? empty : v[1]); + if (empty) scr_dyn_release(empty); + if (checks >= 0) result = sd_check_prime(&x, checks); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 2); + return result; +} + +/* A deferred callback(...args): the callback and its one or two + * arguments, each in a box. */ +static void sd_deliver(ScrClosure *self) { + ScrDyn *callback = (ScrDyn *)scr_box_get_ref(self->caps[0]); + size_t argc = self->ncaps - 1; + ScrDyn *args[2] = {NULL, NULL}; + for (size_t i = 0; i < argc; i++) args[i] = (ScrDyn *)scr_box_get_ref(self->caps[i + 1]); + ScrDyn *result = scr_dyn_call(callback, args, argc, "callback"); + if (result) scr_dyn_release(result); + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); + scr_dyn_release(callback); +} + +/* Calls callback(first[, second]) in a later macrotask (moves first and + * second). */ +static void sd_defer(const ScrDyn *callback, ScrDyn *first, ScrDyn *second) { + ScrDyn *values[3] = {scr_dyn_retain((ScrDyn *)callback), first, second}; + size_t count = second ? 3 : 2; + ScrClosure *c = scr_closure_new((void *)sd_deliver, count); + for (size_t i = 0; i < count; i++) { + c->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[i], values[i]); + } + scr_loop_defer(c); +} + +/* The callback's error argument on success (+1): Node's undefined, or + * null for a callback typed to take Error | null. */ +static ScrDyn *sd_no_error(bool null_error) { + return null_error ? scr_dyn_new_null() : scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); +} + +/* The pending error, taken as a dyn value (+1). */ +static ScrDyn *sd_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *error = scr_caught_to_dyn(caught); + scr_caught_release(caught); + return error; +} + +/* checkPrime(candidate[, options], callback): argument errors throw; the + * callback receives (undefined, verdict) in a later macrotask (null for + * undefined when `null_error`). */ +void scr_crypto_check_prime_async(ScrDyn *candidate, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {candidate, options, callback}; + ScrDyn *v[3]; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + if (sd_unbox_all(v, args, 3) && sd_large_number(&x, "candidate", false, v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } else if (bag->kind == SCR_DYN_UNDEF) { + bag = empty = scr_dyn_new_obj(); + } + int checks = sd_validate_function("callback", fn) ? sd_prime_checks(bag) : -1; + ScrDyn *verdict = checks >= 0 ? sd_check_prime(&x, checks) : NULL; + if (verdict) sd_defer(fn, sd_no_error(null_error), verdict); + if (empty) scr_dyn_release(empty); + } + mbedtls_mpi_free(&x); + sd_release_all(v, 3); +} + +/* RandomPrimeConfig: the size and options checks, then the add and rem + * bounds. False with the error pending. */ +typedef struct { + size_t bits; + bool safe, bigint, has_add, has_rem; + mbedtls_mpi add, rem; +} ScDhPrimeConfig; + +static void sd_prime_config_free(ScDhPrimeConfig *c) { + mbedtls_mpi_free(&c->add); + mbedtls_mpi_free(&c->rem); +} + +static bool sd_prime_size(const ScrDyn *size) { + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return false; + } + return sd_validate_int32_min("size", size->v.num, 1); +} + +static bool sd_prime_config(ScDhPrimeConfig *c, double size, const ScrDyn *options) { + if (!sd_validate_object("options", options)) return false; + const ScrDyn *safe = sd_option(options, "safe"); + const ScrDyn *bigint = sd_option(options, "bigint"); + const ScrDyn *add = sd_option(options, "add"); + const ScrDyn *rem = sd_option(options, "rem"); + if (safe->kind != SCR_DYN_UNDEF && safe->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.safe", "of type boolean", safe); + return false; + } + if (bigint->kind != SCR_DYN_UNDEF && bigint->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.bigint", "of type boolean", bigint); + return false; + } + c->safe = safe->kind == SCR_DYN_BOOL && safe->v.b; + c->bigint = bigint->kind == SCR_DYN_BOOL && bigint->v.b; + c->has_add = add->kind != SCR_DYN_UNDEF; + c->has_rem = rem->kind != SCR_DYN_UNDEF; + if (c->has_add && !sd_large_number(&c->add, "options.add", true, add)) return false; + if (c->has_rem && !sd_large_number(&c->rem, "options.rem", true, rem)) return false; + c->bits = (size_t)size; + if (c->has_add) { + if (mbedtls_mpi_bitlen(&c->add) > c->bits) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.add"); + return false; + } + if (c->has_rem && mbedtls_mpi_cmp_mpi(&c->add, &c->rem) <= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", "invalid options.rem"); + return false; + } + } + if (c->bits > SD_MAX_PRIME_BITS) { + sd_prime_too_long("generatePrime sizes"); + return false; + } + return true; +} + +/* The generated prime as an ArrayBuffer of its bytes, or a bigint (+1), + * or NULL with OpenSSL's error pending. */ +static ScrDyn *sd_prime_result(ScDhPrimeConfig *c) { + mbedtls_mpi p; + mbedtls_mpi_init(&p); + ScrDyn *result = NULL; + if (sd_generate_prime(&p, c->bits, c->safe, c->has_add ? &c->add : NULL, c->has_add && c->has_rem ? &c->rem : NULL)) { + if (c->bigint) { + size_t len = 0; + mbedtls_mpi_write_string(&p, 16, NULL, 0, &len); + char *hex = malloc(len + 2); + if (!hex) scr_trap("scriptc: out of memory\n"); + hex[0] = '0'; + hex[1] = 'x'; + if (mbedtls_mpi_write_string(&p, 16, hex + 2, len, &len) != 0) scr_trap("scriptc: out of memory\n"); + ScrStr *text = scr_str_new(hex, strlen(hex)); + free(hex); + ScrBigInt *value = scr_bigint_parse(text); + scr_str_release(text); + if (value) { + result = scr_dyn_new_bigint(value); + scr_bigint_release(value); + } + } else { + size_t len = mbedtls_mpi_size(&p); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(&p, bytes->data, len) != 0) scr_trap("scriptc: out of memory\n"); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } + } + mbedtls_mpi_free(&p); + return result; +} + +/* generatePrimeSync(size[, options]). */ +ScrDyn *scr_crypto_generate_prime_sync(ScrDyn *size, ScrDyn *options) { + ScrDyn *args[2] = {size, options}; + ScrDyn *v[2]; + ScrDyn *result = NULL; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 2) && sd_prime_size(v[0])) { + ScrDyn *empty = v[1]->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + if (sd_prime_config(&c, v[0]->v.num, empty ? empty : v[1])) result = sd_prime_result(&c); + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 2); + return result; +} + +/* generatePrime(size[, options], callback): argument errors throw; the + * callback receives (undefined, prime), or OpenSSL's error alone, in a + * later macrotask (null for undefined when `null_error`). */ +void scr_crypto_generate_prime_async(ScrDyn *size, ScrDyn *options, ScrDyn *callback, bool null_error) { + ScrDyn *args[3] = {size, options, callback}; + ScrDyn *v[3]; + ScDhPrimeConfig c = {0}; + mbedtls_mpi_init(&c.add); + mbedtls_mpi_init(&c.rem); + if (sd_unbox_all(v, args, 3) && sd_prime_size(v[0])) { + ScrDyn *bag = v[1], *fn = v[2]; + ScrDyn *empty = NULL; + if (scr_dyn_is_callable(bag)) { + fn = bag; + bag = empty = scr_dyn_new_obj(); + } + if (sd_validate_function("callback", fn) && sd_prime_config(&c, v[0]->v.num, bag)) { + ScrDyn *prime = sd_prime_result(&c); + if (prime) sd_defer(fn, sd_no_error(null_error), prime); + else sd_defer(fn, sd_take_error(), NULL); + } + if (empty) scr_dyn_release(empty); + } + sd_prime_config_free(&c); + sd_release_all(v, 3); +} + /* ── methods ──────────────────────────────────────────────────────────── */ static ScrDyn *sd_arg(ScrDyn *const *args, size_t argc, size_t index) { diff --git a/tests/corpus/7150-cipher-legacy-modes.ts b/tests/corpus/7150-cipher-legacy-modes.ts new file mode 100644 index 000000000..80edfd029 --- /dev/null +++ b/tests/corpus/7150-cipher-legacy-modes.ts @@ -0,0 +1,74 @@ +// createCipheriv and createDecipheriv in the legacy modes: AES in ECB, +// CFB, CFB8, CFB1 and OFB, triple DES (three- and two-key) in CBC, ECB, +// CFB, CFB8, CFB1 and OFB, and bare chacha20. Fixed keys, IVs and inputs +// give Node's exact ciphertexts; chunked updates, string encodings, +// disabled padding and chacha20's 64-bit counter carry round-trip. +import { createCipheriv, createDecipheriv } from "node:crypto"; + +function bytes(length: number, seed: number): Buffer { + const out = Buffer.alloc(length); + for (let i = 0; i < length; i++) out[i] = (i * 29 + seed) & 0xff; + return out; +} + +const plaintext = Buffer.from("The five boxing wizards jump quickly; sphinx of black quartz, judge my vow."); + +function encrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const cipher = createCipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(cipher.update(data.subarray(off, off + chunk))); + parts.push(cipher.final()); + return Buffer.concat(parts); +} + +function decrypt(name: string, key: Buffer, iv: Buffer | null, data: Buffer, chunk: number): Buffer { + const decipher = createDecipheriv(name, key, iv); + const parts: Buffer[] = []; + for (let off = 0; off < data.length; off += chunk) parts.push(decipher.update(data.subarray(off, off + chunk))); + parts.push(decipher.final()); + return Buffer.concat(parts); +} + +const cases: [string, number, number][] = []; +for (const bits of [128, 192, 256]) + for (const mode of ["ecb", "cfb", "cfb8", "cfb1", "ofb"]) cases.push([`aes-${bits}-${mode}`, bits / 8, mode === "ecb" ? 0 : 16]); +for (const name of ["des-ede3-cbc", "des3", "des-ede3", "des-ede3-ecb", "des-ede3-cfb", "des-ede3-cfb8", "des-ede3-cfb1", "des-ede3-ofb"]) + cases.push([name, 24, name === "des-ede3" || name === "des-ede3-ecb" ? 0 : 8]); +for (const name of ["des-ede-cbc", "des-ede", "des-ede-ecb", "des-ede-cfb", "des-ede-ofb"]) + cases.push([name, 16, name === "des-ede" || name === "des-ede-ecb" ? 0 : 8]); +cases.push(["chacha20", 32, 16]); + +for (const [name, keyLength, ivLength] of cases) { + const key = bytes(keyLength, keyLength + 3); + const iv = ivLength ? bytes(ivLength, 11) : null; + const sealed = encrypt(name, key, iv, plaintext, plaintext.length); + const chunked = encrypt(name, key, iv, plaintext, 7); + const opened = decrypt(name, key, iv, sealed, 5); + console.log(name.padEnd(14), sealed.length, sealed.toString("hex").slice(0, 48), chunked.equals(sealed), opened.equals(plaintext)); +} + +// String input and output encodings through a stream mode. +const ofb = createCipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +const encoded = ofb.update("68656c6c6f20776f726c64", "hex", "base64") + ofb.final("base64"); +const unofb = createDecipheriv("aes-128-ofb", bytes(16, 1), bytes(16, 2)); +console.log(encoded, unofb.update(encoded, "base64", "utf8") + unofb.final("utf8")); + +// ECB and triple-DES CBC without padding over whole blocks. +for (const [name, keyLength, ivLength, block] of [["aes-256-ecb", 32, 0, 16], ["des-ede3-cbc", 24, 8, 8]] as const) { + const cipher = createCipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + cipher.setAutoPadding(false); + const sealed = Buffer.concat([cipher.update(bytes(block * 3, 7)), cipher.final()]); + const decipher = createDecipheriv(name, bytes(keyLength, 5), ivLength ? bytes(ivLength, 6) : null); + decipher.setAutoPadding(false); + const opened = Buffer.concat([decipher.update(sealed), decipher.final()]); + console.log(name, "unpadded", sealed.length, sealed.toString("hex").slice(0, 32), opened.equals(bytes(block * 3, 7))); +} + +// chacha20's counter is the IV's first 8 bytes, little-endian: the low +// word carries into the high word, which wraps. +const chachaKey = bytes(32, 9); +for (const counter of ["ffffffff00000000", "ffffffffffffffff", "fffffffe01000000"]) { + const iv = Buffer.from(counter + "0102030405060708", "hex"); + const stream = encrypt("chacha20", chachaKey, iv, Buffer.alloc(200), 50); + console.log("chacha20", counter, stream.toString("hex").slice(120, 152), stream.subarray(64, 96).toString("hex")); +} diff --git a/tests/corpus/7151-cipher-legacy-errors.cjs b/tests/corpus/7151-cipher-legacy-errors.cjs new file mode 100644 index 000000000..fe9c59bcc --- /dev/null +++ b/tests/corpus/7151-cipher-legacy-errors.cjs @@ -0,0 +1,83 @@ +// createCipheriv errors for the legacy modes, in Node's order: the IV +// length each mode takes (none for ECB), the key length, OpenSSL's +// "unsupported" for the ciphers its legacy provider holds, and the block +// modes' final-block and padding failures; the AEAD-only methods throw +// Node's invalid-state errors on the stream modes. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", Buffer.isBuffer(value) ? value.toString("hex") : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, e.library, e.reason, JSON.stringify(e.opensslErrorStack)); + } +} + +const k16 = Buffer.alloc(16, 1), k24 = Buffer.alloc(24, 1), k32 = Buffer.alloc(32, 1); +attempt("ecb null iv", () => crypto.createCipheriv("aes-128-ecb", k16, null).final()); +attempt("ecb empty iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(0)).final()); +attempt("ecb 16-byte iv", () => crypto.createCipheriv("aes-128-ecb", k16, Buffer.alloc(16))); +attempt("cfb 15-byte iv", () => crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(15))); +attempt("cfb null iv", () => crypto.createCipheriv("aes-128-cfb", k16, null)); +attempt("ofb key 15", () => crypto.createCipheriv("aes-128-ofb", Buffer.alloc(15), Buffer.alloc(16))); +attempt("des3 16-byte iv", () => crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(16))); +attempt("des3 16-byte key", () => crypto.createCipheriv("des-ede3-cbc", k16, Buffer.alloc(8))); +attempt("des-ede 24-byte key", () => crypto.createCipheriv("des-ede-cbc", k24, Buffer.alloc(8))); +attempt("des-ede3 ecb with iv", () => crypto.createCipheriv("des-ede3", k24, Buffer.alloc(8))); +attempt("chacha20 12-byte iv", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(12))); +attempt("chacha20 authTagLength ignored", () => crypto.createCipheriv("chacha20", k32, Buffer.alloc(16), { authTagLength: 16 }).update("x")); +attempt("upper-case name", () => crypto.createCipheriv("DES-EDE3-CBC", k24, Buffer.alloc(8)).final()); +attempt("des-cbc", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(8))); +attempt("des-cbc bad iv", () => crypto.createCipheriv("des-cbc", Buffer.alloc(8), Buffer.alloc(3))); +attempt("des-cbc bad key", () => crypto.createCipheriv("des-cbc", Buffer.alloc(3), Buffer.alloc(8))); +attempt("bf-cbc", () => crypto.createDecipheriv("bf-cbc", k16, Buffer.alloc(8))); +attempt("rc4", () => crypto.createCipheriv("rc4", k16, null)); +attempt("rc4 with iv", () => crypto.createCipheriv("rc4", k16, Buffer.alloc(8))); +attempt("seed-ecb", () => crypto.createCipheriv("seed-ecb", k16, null)); +attempt("unknown cipher", () => crypto.createCipheriv("aes-128-xyz", k16, Buffer.alloc(16))); + +attempt("cfb getAuthTag", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + c.update("x"); + c.final(); + return c.getAuthTag(); +}); +attempt("ofb setAAD", () => crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)).setAAD(Buffer.alloc(1))); +attempt("chacha20 setAuthTag", () => crypto.createDecipheriv("chacha20", k32, Buffer.alloc(16)).setAuthTag(Buffer.alloc(16))); +attempt("cfb update sizes", () => { + const c = crypto.createCipheriv("aes-128-cfb", k16, Buffer.alloc(16)); + return [c.update("abc").length, c.update("defghijklmnopqrstu").length, c.final().length].join(","); +}); +attempt("cfb1 setAutoPadding(false)", () => { + const c = crypto.createCipheriv("aes-128-cfb1", k16, Buffer.alloc(16)); + c.setAutoPadding(false); + return Buffer.concat([c.update("abc"), c.final()]); +}); +attempt("ecb unpadded short", () => { + const c = crypto.createCipheriv("aes-128-ecb", k16, null); + c.setAutoPadding(false); + c.update(Buffer.alloc(31, 7)); + return c.final(); +}); +attempt("des3 unpadded short", () => { + const c = crypto.createCipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + c.setAutoPadding(false); + c.update(Buffer.alloc(7)); + return c.final(); +}); +attempt("des3 bad decrypt", () => { + const d = crypto.createDecipheriv("des-ede3-cbc", k24, Buffer.alloc(8)); + d.update(Buffer.alloc(16, 9)); + return d.final(); +}); +attempt("ecb decrypt 15 bytes", () => { + const d = crypto.createDecipheriv("aes-128-ecb", k16, null); + d.update(Buffer.alloc(15)); + return d.final(); +}); +attempt("ofb update after final", () => { + const c = crypto.createCipheriv("aes-128-ofb", k16, Buffer.alloc(16)); + c.final(); + return c.update("x"); +}); diff --git a/tests/corpus/7152-crypto-prime.ts b/tests/corpus/7152-crypto-prime.ts new file mode 100644 index 000000000..79edcc502 --- /dev/null +++ b/tests/corpus/7152-crypto-prime.ts @@ -0,0 +1,96 @@ +// checkPrimeSync, checkPrime, generatePrimeSync and generatePrime: known +// primes, composites and strong pseudoprimes as bigints, Buffers, typed +// arrays, ArrayBuffers and DataViews, the checks option, and generated +// primes' length, top bits, primality, residues and safety, as +// ArrayBuffers and bigints, synchronously and through the callbacks (whose +// error parameter, typed Error | null, receives null where Node passes +// undefined). The small sizes and the add and rem forms have one possible +// result. +import { checkPrime, checkPrimeSync, generatePrime, generatePrimeSync } from "node:crypto"; + +const mersenne61 = 2n ** 61n - 1n; +const mersenne127 = 2n ** 127n - 1n; +const mersenne521 = 2n ** 521n - 1n; +const known: [string, bigint][] = [ + ["0", 0n], + ["1", 1n], + ["2", 2n], + ["3", 3n], + ["4", 4n], + ["9", 9n], + ["17863", 17863n], + ["17881", 17881n], + ["561 (Carmichael)", 561n], + ["2047 (strong pseudoprime to 2)", 2047n], + ["3215031751 (strong pseudoprime to 2, 3, 5 and 7)", 3215031751n], + ["2^61 - 1", mersenne61], + ["2^61 + 1", mersenne61 + 2n], + ["2^127 - 1", mersenne127], + ["(2^61 - 1)(2^127 - 1)", mersenne61 * mersenne127], + ["2^521 - 1", mersenne521], + ["2^521 + 1", mersenne521 + 2n], +]; +for (const [label, value] of known) console.log(label, checkPrimeSync(value)); + +console.log("Buffer 13", checkPrimeSync(Buffer.from([0, 0, 13]))); +console.log("Uint8Array 15", checkPrimeSync(new Uint8Array([15]))); +const words = new ArrayBuffer(8); +new DataView(words).setBigUint64(0, mersenne61); +console.log("ArrayBuffer 2^61 - 1", checkPrimeSync(words)); +console.log("DataView 2^61 - 1", checkPrimeSync(new DataView(words))); +console.log("empty", checkPrimeSync(new Uint8Array(0))); +console.log("checks 1", checkPrimeSync(97n, { checks: 1 })); +console.log("checks 200", checkPrimeSync(3215031751n, { checks: 200 })); + +const bitLength = (n: bigint): number => n.toString(2).length; +const fromBuffer = (buffer: ArrayBuffer): bigint => BigInt("0x" + Buffer.from(buffer).toString("hex")); + +for (const bits of [2, 3, 4]) console.log("size", bits, generatePrimeSync(bits, { bigint: true })); +const five = generatePrimeSync(5, { bigint: true }); +console.log("size 5", five === 29n || five === 31n); +for (const bits of [8, 16, 31, 32, 33, 64, 127, 256, 512]) { + const p = generatePrimeSync(bits, { bigint: true }); + console.log("size", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p)); +} +const buffer = generatePrimeSync(96); +console.log("ArrayBuffer", buffer instanceof ArrayBuffer, buffer.byteLength, checkPrimeSync(buffer), bitLength(fromBuffer(buffer))); +console.log("bigint false", generatePrimeSync(40, { bigint: false }).byteLength); + +for (const bits of [3, 6, 7]) console.log("safe", bits, generatePrimeSync(bits, { safe: true, bigint: true })); +for (const bits of [16, 64, 128]) { + const p = generatePrimeSync(bits, { safe: true, bigint: true }); + console.log("safe", bits, bitLength(p) === bits, p >> BigInt(bits - 2), checkPrimeSync(p), checkPrimeSync((p - 1n) / 2n)); +} + +console.log("add 200 rem 1", generatePrimeSync(8, { add: 200n, rem: 1n, bigint: true })); +console.log("add 128", generatePrimeSync(8, { add: 128n, bigint: true })); +console.log("add 16 rem 3", generatePrimeSync(5, { add: 16n, rem: 3n, bigint: true })); +const safe4 = generatePrimeSync(5, { add: 4n, safe: true, bigint: true }); +console.log("add 4 safe", safe4 === 23n || safe4 === 47n); +for (const [label, add, rem] of [ + ["add 12 rem 11", 12n, 11n], + ["add 6", 6n, undefined], + ["add 1000 rem 7", 1000n, 7n], +] as const) { + const p = generatePrimeSync(48, rem === undefined ? { add, bigint: true } : { add, rem, bigint: true }); + console.log(label, p % add, checkPrimeSync(p), bitLength(p) >= 48); +} +const viaBuffer = generatePrimeSync(64, { add: Buffer.from([0x01, 0x00]), rem: new Uint8Array([0x0f]), bigint: true }); +console.log("add and rem as bytes", viaBuffer % 256n, checkPrimeSync(viaBuffer)); +const safeAdd = generatePrimeSync(64, { add: 24n, rem: 23n, safe: true, bigint: true }); +console.log("safe add 24 rem 23", safeAdd % 24n, checkPrimeSync((safeAdd - 1n) / 2n)); +console.log("rem without add", generatePrimeSync(16, { rem: 5n, bigint: true }) < 65536n); + +generatePrime(64, (err, prime) => { + console.log("callback", err ?? "no error", prime instanceof ArrayBuffer, prime.byteLength, checkPrimeSync(prime)); + generatePrime(128, { safe: true, bigint: true }, (err2, safe) => { + console.log("callback safe", err2 ?? "no error", bitLength(safe), checkPrimeSync((safe - 1n) / 2n)); + checkPrime(safe, (err3, result) => { + console.log("checkPrime", err3 ?? "no error", result); + checkPrime(Buffer.from([1, 1]), { checks: 3 }, (err4, result2) => { + console.log("checkPrime 257", err4 ?? "no error", result2); + }); + }); + }); +}); +console.log("sync done"); diff --git a/tests/corpus/7153-crypto-prime-errors.cjs b/tests/corpus/7153-crypto-prime-errors.cjs new file mode 100644 index 000000000..ff32e6e09 --- /dev/null +++ b/tests/corpus/7153-crypto-prime-errors.cjs @@ -0,0 +1,80 @@ +// checkPrime, checkPrimeSync, generatePrime and generatePrimeSync errors from +// JavaScript: argument types and ranges, the options bag, add and rem, and +// OpenSSL's generation failures, synchronously and through the callbacks. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const value = run(); + console.log(label, "ok", typeof value === "bigint" ? `${value}n` : value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +attempt("check string", () => crypto.checkPrimeSync("7")); +attempt("check number", () => crypto.checkPrimeSync(7)); +attempt("check negative", () => crypto.checkPrimeSync(-7n)); +attempt("check large negative", () => crypto.checkPrimeSync(-(2n ** 40n))); +attempt("check options null", () => crypto.checkPrimeSync(7n, null)); +attempt("check options array", () => crypto.checkPrimeSync(7n, [])); +attempt("check options string", () => crypto.checkPrimeSync(7n, "x")); +attempt("check checks string", () => crypto.checkPrimeSync(7n, { checks: "1" })); +attempt("check checks fraction", () => crypto.checkPrimeSync(7n, { checks: 1.5 })); +attempt("check checks -1", () => crypto.checkPrimeSync(7n, { checks: -1 })); +attempt("check checks 2^31", () => crypto.checkPrimeSync(7n, { checks: 2 ** 31 })); +attempt("check checks -0", () => crypto.checkPrimeSync(7n, { checks: -0 })); +attempt("check checks 10", () => crypto.checkPrimeSync(7n, { checks: 10 })); +attempt("check no callback", () => crypto.checkPrime(7n)); +attempt("check callback string", () => crypto.checkPrime(7n, {}, "cb")); +attempt("check async string", () => crypto.checkPrime("7", () => {})); +attempt("check async options null", () => crypto.checkPrime(7n, null, () => {})); + +attempt("size string", () => crypto.generatePrimeSync("8")); +attempt("size fraction", () => crypto.generatePrimeSync(8.5)); +attempt("size 0", () => crypto.generatePrimeSync(0)); +attempt("size 2^31", () => crypto.generatePrimeSync(2 ** 31)); +attempt("size NaN", () => crypto.generatePrimeSync(NaN)); +attempt("size 1", () => crypto.generatePrimeSync(1)); +attempt("size 2 safe", () => crypto.generatePrimeSync(2, { safe: true })); +attempt("size 4 safe", () => crypto.generatePrimeSync(4, { safe: true })); +attempt("size 5 safe", () => crypto.generatePrimeSync(5, { safe: true })); +attempt("options null", () => crypto.generatePrimeSync(8, null)); +attempt("options number", () => crypto.generatePrimeSync(8, 1)); +attempt("safe string", () => crypto.generatePrimeSync(8, { safe: "yes" })); +attempt("bigint number", () => crypto.generatePrimeSync(8, { bigint: 1 })); +attempt("add number", () => crypto.generatePrimeSync(8, { add: 6 })); +attempt("add string", () => crypto.generatePrimeSync(8, { add: "6" })); +attempt("add negative", () => crypto.generatePrimeSync(8, { add: -6n })); +attempt("rem number", () => crypto.generatePrimeSync(8, { add: 6n, rem: 1 })); +attempt("rem negative", () => crypto.generatePrimeSync(8, { add: 6n, rem: -1n })); +attempt("add too long", () => crypto.generatePrimeSync(8, { add: 1024n })); +attempt("add zero", () => crypto.generatePrimeSync(8, { add: 0n })); +attempt("add zero bytes", () => crypto.generatePrimeSync(8, { add: new Uint8Array(0) })); +attempt("rem equal add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 6n })); +attempt("rem above add", () => crypto.generatePrimeSync(8, { add: 6n, rem: 7n })); +attempt("rem without add", () => crypto.generatePrimeSync(8, { rem: 1000n, bigint: true }) < 256n); +attempt("add 1 bigint", () => crypto.generatePrimeSync(8, { add: 1n, bigint: true }) < 256n); +attempt("gen no callback", () => crypto.generatePrime(8)); +attempt("gen options callback", () => crypto.generatePrime(8, {})); +attempt("gen size string", () => crypto.generatePrime("8", () => {})); +attempt("gen add too long", () => crypto.generatePrime(8, { add: 1024n }, () => {})); +attempt("gen options null", () => crypto.generatePrime(8, null, () => {})); + +// The callback forms' generation errors and results, one after another. +crypto.generatePrime(1, function (err, prime) { + console.log("gen 1 callback", arguments.length, err.name, err.code, err.message, JSON.stringify(Object.keys(err)), prime); + crypto.generatePrime(8, { add: 0n }, function (err2, prime2) { + console.log("gen add 0 callback", arguments.length, err2.message, prime2); + crypto.generatePrime(3, { safe: true, bigint: true }, function (err3, prime3) { + console.log("gen 3 safe callback", arguments.length, err3, prime3); + crypto.checkPrime(7n, function (err4, result) { + console.log("check 7 callback", arguments.length, err4, result); + crypto.checkPrime(new Uint8Array([0, 0, 9]), { checks: 3 }, function (err5, result2) { + console.log("check 9 callback", arguments.length, err5, result2); + }); + }); + }); + }); +}); +console.log("sync end"); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 644e901f4..27e68a618 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -70,12 +70,12 @@ http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamRese http2.connect("https://localhost"); /* The crypto surface beyond the lowered slice (randomness, the hash * chain, the introspection statics): RSA encryption names the lowered - * asymmetric surface, symmetric ciphers the missing cipher stack, prime - * checks theirs, and setFips the FIPS truth (getFips() lowers to 0). */ -import { checkPrimeSync, createCipheriv, publicEncrypt, setFips } from "node:crypto"; + * asymmetric surface, symmetric ciphers the missing cipher stack, the + * 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 }); -checkPrimeSync(7n); +secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { integrity: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 133978b5e..cabce49c4 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -117,7 +117,7 @@ fenced.ts:69:71 - error SC2020: 'createSecureServer option 'streamResetBurst' wi fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node but has no scriptc lowering yet - 75 | import { checkPrimeSync, createCipheriv, publicEncrypt, setFips } from "node:crypto"; + 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 }); @@ -129,22 +129,22 @@ fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm' 76 | publicEncrypt("key", Buffer.alloc(1)); 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); - hint: the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305 + 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 -fenced.ts:78:1 - error SC2020: 'crypto.checkPrimeSync' is typed by @types/node but has no scriptc lowering yet +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 }); - 78 | checkPrimeSync(7n); - | ^~~~~~~~~~~~~~~~~~ + 78 | secureHeapUsed(); + | ^~~~~~~~~~~~~~~~ 79 | setFips(false); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) fenced.ts:79:1 - error SC2020: 'crypto.setFips' is typed by @types/node but has no scriptc lowering yet - 78 | checkPrimeSync(7n); + 78 | secureHeapUsed(); 79 | setFips(false); | ^~~~~~~~~~~~~~ 80 | fetch("https://example.invalid/", { diff --git a/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json new file mode 100644 index 000000000..40196decf --- /dev/null +++ b/tests/harness/corpus-costs/7150-cipher-legacy-modes.ts.json @@ -0,0 +1 @@ +{ "plain": 6099, "san": 8251 } diff --git a/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json new file mode 100644 index 000000000..6a82dd081 --- /dev/null +++ b/tests/harness/corpus-costs/7151-cipher-legacy-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 7845, "san": 9581 } diff --git a/tests/harness/corpus-costs/7152-crypto-prime.ts.json b/tests/harness/corpus-costs/7152-crypto-prime.ts.json new file mode 100644 index 000000000..4df2be436 --- /dev/null +++ b/tests/harness/corpus-costs/7152-crypto-prime.ts.json @@ -0,0 +1 @@ +{ "plain": 7273, "san": 10284 } diff --git a/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json new file mode 100644 index 000000000..d20dc198a --- /dev/null +++ b/tests/harness/corpus-costs/7153-crypto-prime-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 5893, "san": 5691 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index b5fb028f9..c8b0b51b8 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -847,8 +847,9 @@ 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-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, " + - "aes-192-gcm, aes-256-gcm and chacha20-poly1305\n" + + "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 ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n", ); expect(r.stderr).toBe(""); From 091f280ceced8d2df2e7e1745f6e3527b797a1c4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:32:19 -0500 Subject: [PATCH 035/191] Read the union payload inline when comparing it with a number or boolean `u === 0`, `u !== true` and `Object.is(u, x)` against a scalar arm tested the union's tag inline but read its payload through scr_union_get_f64 or scr_union_get_bool. The call passed the union's stack box to the runtime, so LLVM could not promote the box to registers, and an element read of a `number[]` compared with a number kept its value, tag and box in memory. The comparison now loads the payload slot directly, as other union extractions already do, and the box no longer escapes. --- .../compiler/src/backend/llvm/expr-dynamic.ts | 8 +-- .../corpus/7322-union-scalar-equality.ts.json | 6 ++ .../union-scalar-equality-emission.test.ts | 45 ++++++++++++ tests/corpus/7322-union-scalar-equality.ts | 68 +++++++++++++++++++ .../7322-union-scalar-equality.ts.json | 1 + 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json create mode 100644 packages/compiler/test/union-scalar-equality-emission.test.ts create mode 100644 tests/corpus/7322-union-scalar-equality.ts create mode 100644 tests/harness/corpus-costs/7322-union-scalar-equality.ts.json diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 563a900f8..819358aad 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -1282,9 +1282,7 @@ function emitUnionEqWrappedScalar( B.startBlock(same); const eq = B.tmp(); if (arm.kind === "f64") { - host.declare(`declare double @scr_union_get_f64(ptr)`); - const v = B.tmp(); - B.line(`${v} = call double @scr_union_get_f64(ptr ${union.name})`); + const v = host.unionGetF64(union.name); if (e.sameValue) { host.declare(`declare zeroext i1 @scr_num_same_value(double, double)`); B.line(`${eq} = call zeroext i1 @scr_num_same_value(double ${v}, double ${plain.name})`); @@ -1292,9 +1290,7 @@ function emitUnionEqWrappedScalar( B.line(`${eq} = fcmp oeq double ${v}, ${plain.name}`); } } else { - host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`); - const v = B.tmp(); - B.line(`${v} = call zeroext i1 @scr_union_get_bool(ptr ${union.name})`); + const v = host.unionGetBool(union.name); B.line(`${eq} = icmp eq i1 ${v}, ${plain.name}`); } B.line(`store i1 ${eq}, ptr ${slot}`); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..73257a3f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7322-union-scalar-equality.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7322-union-scalar-equality.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/union-scalar-equality-emission.test.ts b/packages/compiler/test/union-scalar-equality-emission.test.ts new file mode 100644 index 000000000..62d59e258 --- /dev/null +++ b/packages/compiler/test/union-scalar-equality-emission.test.ts @@ -0,0 +1,45 @@ +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-union-scalar-eq-")); + 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 zeros(a: number[], at: number[]): number { + let n = 0; + for (const i of at) if (a[i] === 0) n++; + return n; +} +function flags(a: (boolean | undefined)[]): number { + let n = 0; + for (let i = 0; i < a.length; i++) if (a[i] === true) n++; + return n; +} +console.log(zeros([0, 1, 0], [0, 1, 2, 5]), flags([true, undefined, false])); +`; + +test("comparing a union with a number or boolean reads its payload inline", async () => { + const llvm = await emitLlvm(source); + for (const name of ["zeros", "flags"]) { + const fn = new RegExp(`^define internal double @sc_bf_${name}\\([^]*?^}`, "m").exec(llvm)?.[0]; + expect(fn, name).toBeDefined(); + // The tag test and the payload load stay in the function, so the + // union's stack box has no escaping use. + expect(fn).not.toMatch(/@scr_union_get_(f64|bool)\(/); + expect(fn).toMatch(/getelementptr inbounds %ScrUnion, ptr %\S+, i64 0, i32 5/); + } +}); diff --git a/tests/corpus/7322-union-scalar-equality.ts b/tests/corpus/7322-union-scalar-equality.ts new file mode 100644 index 000000000..82e0564fc --- /dev/null +++ b/tests/corpus/7322-union-scalar-equality.ts @@ -0,0 +1,68 @@ +// Strict equality and inequality between a `number | undefined` or +// `boolean | undefined` value and a number or boolean: element reads of +// holes, past the end and of present undefined, NaN, -0 and +0, and +// Object.is, in branches, loops and expressions. + +function zeros(a: number[], at: number[]): string { + const hits: string[] = []; + for (const i of at) { + const v = a[i]; + hits.push(`${i}:${v === 0 ? "eq" : "ne"}${v !== 0 ? "!" : ""}${0 === v ? "r" : ""}`); + } + return hits.join(" "); +} + +const holey: number[] = [0, -0, 1]; +holey[5] = 0; +console.log(zeros(holey, [0, 1, 2, 3, 4, 5, 6, -1])); +console.log(zeros([NaN, 0.5, -1, 0], [0, 1, 2, 3])); + +function nanAndZeros(a: (number | undefined)[]): string { + return a + .map((v) => `${v === -0}${v === 0}${Object.is(v, -0)}${Object.is(v, 0)}${Object.is(v, NaN)}`) + .join(" "); +} +console.log(nanAndZeros([0, -0, NaN, undefined, 1])); + +function flags(a: (boolean | undefined)[]): string { + let t = 0; + let f = 0; + let notTrue = 0; + for (let i = 0; i < a.length + 2; i++) { + if (a[i] === true) t++; + if (a[i] === false) f++; + if (a[i] !== true) notTrue++; + } + return `${t} ${f} ${notTrue}`; +} +const bools: (boolean | undefined)[] = [true, false, undefined, true]; +bools[7] = false; +console.log(flags(bools)); + +// A permutation walk that stops at a zero element, as the flip loops of a +// counting benchmark do. +function walk(p: number[], start: number): number { + let steps = 0; + let at = start; + while (p[at] !== 0 && steps < 20) { + at = p[at]!; + steps++; + } + return steps; +} +console.log(walk([0, 3, 1, 2], 1), walk([1, 2, 3, 0], 0), walk([2, 2, 2], 0)); + +class Slot { + value: number | undefined = undefined; + on: boolean | undefined = undefined; +} +const slots = [new Slot(), new Slot(), new Slot()]; +slots[1]!.value = 0; +slots[1]!.on = true; +slots[2]!.value = -0; +slots[2]!.on = false; +console.log( + slots + .map((s) => `${s.value === 0}${s.value !== 0}${s.on === true}${s.on === false}${s.on !== false}`) + .join(" "), +); diff --git a/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json new file mode 100644 index 000000000..2df20a0ed --- /dev/null +++ b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } From 2d6ef06fcb04bcf46a1b0f789f86efb0dde5b649 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:40:22 -0500 Subject: [PATCH 036/191] Version while loops over typed arrays on their counters' bounds Typed-array loop versions applied only to for loops, so a scanner written with while loops, such as `while (pos < end && bytes[pos] === 32) pos++`, kept one bounds test per access. A while loop counter is now a number local that the loop changes only by positive integer increments (`c = c + k`, `c++`, `c = (c + k) | 0`) written as statements of its body, blocks, branches and nested while loops. An access at `counter + d` after a condition compared the counter below a loop-invariant bound, with no increment since, is in bounds for the whole loop when the guard finds the counter's entry value an exact integer of at least -d and the bound at most the length minus the access's extent. A counter with a `| 0` increment also needs its loops' bounds at most 2^31 minus its increments, so it never wraps. A fixed check (a statically ranged index) now needs an end below 2^31. The counter of a loop to a number[]'s length whose length the loop may change reaches 2^32 - 2, and its typed-array accesses got a guard no typed array passes and a second copy of the loop. --- .../compiler/src/backend/llvm/byte-windows.ts | 1 + .../backend/llvm/bytes-loop-versions.test.ts | 62 +++ .../src/backend/llvm/bytes-loop-versions.ts | 490 +++++++++++++++++- .../src/backend/llvm/emit-statements.ts | 81 +-- packages/compiler/src/backend/llvm/emitter.ts | 2 +- ...98-typed-array-while-loop-versions.ts.json | 6 + .../6998-typed-array-while-loop-versions.ts | 149 ++++++ ...98-typed-array-while-loop-versions.ts.json | 1 + 8 files changed, 746 insertions(+), 46 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json create mode 100644 tests/corpus/6998-typed-array-while-loop-versions.ts create mode 100644 tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json diff --git a/packages/compiler/src/backend/llvm/byte-windows.ts b/packages/compiler/src/backend/llvm/byte-windows.ts index 5fffa1428..9741cc547 100644 --- a/packages/compiler/src/backend/llvm/byte-windows.ts +++ b/packages/compiler/src/backend/llvm/byte-windows.ts @@ -43,6 +43,7 @@ export function findInitializedByteLoopBindings( if (stmt.init) list([stmt.init], inner); list(stmt.body, inner); } + if (stmt.kind === "while") entries.set(stmt, new Set(known)); if (stmt.kind === "while" || stmt.kind === "doWhile") list(stmt.body, new Set(known)); } } 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..2d70e9014 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -177,6 +177,11 @@ test("statically ranged indices compare one constant with the length", () => { ]); expect(version.affine).toEqual([]); expect(version.counted).toBeNull(); + // A uint32 index (an array counter's range) gets no guard: no typed array + // in practice has 2^31 elements. + const wide = bin(">>>", bin("^", ref("i"), ref("k")), num(0)); + const unbounded = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), wide))]); + expect(match(fn([unbounded]), unbounded)!.fixed).toEqual([{ receiver: 0, end: 0 }]); }); test("scaled counters and body temps resolve to their receiver and index", () => { @@ -358,6 +363,63 @@ test("index temps of a version's counter are integers in the fast copy", () => { expect(fast).not.toContain("bytes.index"); }); +test("while loop counters below a bound version their accesses", () => { + // while (i < n) { out[i] = src[i + 1]; i = i + 1 } + const step: IrStmt = { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }; + const whileLoop = (body: IrStmt[]): IrStmt & { kind: "while" } => ({ + kind: "while", + cond: { kind: "bin", op: "<", left: ref("i"), right: ref("n"), type: BOOL, loc }, + body, + loc, + }); + const copy = whileLoop([ + set(ref("out", f64s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))), + step, + ]); + const declared = (loop: IrStmt): IrFunction => + fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]); + const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => { + const f = declared(loop); + const ranges = analyzeIntegerRanges(f); + return matchBytesLoopVersion(loop, null, { + locals: new Map(f.locals.map((l) => [l.id, l])), + invariantGlobals: new Set(), + captured: new Set(), + ranges, + initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), + proven: new Set(), + workers: false, + owned: new Set(), + }); + }; + const version = versionOf(copy)!; + expect(version.bounds.size).toBe(2); + expect(version.bounded.map((c) => [version.receivers[c.receiver]!.ref, c.slack])).toEqual([ + [ref("out", f64s), 0], + [ref("src", f64s), 1], + ]); + // `out[i]` needs `i` to start at 0 or above (`src[i + 1]` only at -1). + expect(version.counters.map((c) => [c.ref.kind === "varRef" && c.ref.localId, c.min])).toEqual([ + ["i", 0], + ]); + // An access after the counter advances in the same pass stays checked. + const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]); + expect(versionOf(advanced)).toBeNull(); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + declared(copy), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.index"); + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index c3cee3feb..81f0e9eee 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; @@ -23,6 +23,24 @@ interface KeyedTerm extends Term { key: string; } +/** Every access of `receiver` at a while loop counter `c + d` happens while + * `c < bound` (`c <= bound` when `inclusive`) holds and before the counter + * advances, so it is in bounds when `bound + slack <= length`. */ +interface BoundedCheck { + receiver: number; + bound: IrExpr; + slack: number; +} + +/** A while loop counter whose entry value the guard tests: an exact + * integer of at least `min`. A counter that `| 0` wraps also needs every + * bound of its loops at most `wrapLimit`, so that it never reaches 2^31. */ +interface CounterCheck { + ref: VarRef; + min: number; + wrapBounds: { bound: IrExpr; limit: number }[]; +} + /** Every access of `receiver` at a static index range ends below `end + 1`. */ interface FixedCheck { receiver: number; @@ -51,6 +69,9 @@ export interface BytesLoopVersion { ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; + /** While loops: accesses at a counter below a loop-invariant bound. */ + bounded: BoundedCheck[]; + counters: CounterCheck[]; /** Keys of affine terms the guard tests at run time: exact integers * within GUARDED_TERM. */ guarded: ReadonlySet; @@ -83,7 +104,7 @@ export interface BytesLoopFacts { } const MAX_COST = 400; -const MAX_FIXED_END = 2 ** 32; +const MAX_FIXED_END = 2 ** 31; const MAX_CHECKS = 8; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; @@ -116,7 +137,7 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * whenever the guard fails, and keeps every out-of-bounds read, ignored * write and NaN-index behavior. */ export function matchBytesLoopVersion( - loop: IrStmt & { kind: "for" }, + loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, facts: BytesLoopFacts, ): BytesLoopVersion | null { @@ -155,10 +176,10 @@ export function matchBytesLoopVersion( // writes is not invariant either: `for (let i = (a = b, 0); ...)` must // not keep the bounds the guard proved for the old `a`. if ( - (loop.init !== null && !scanStmt(loop.init)) || + (loop.kind === "for" && loop.init !== null && !scanStmt(loop.init)) || !loop.body.every(scanStmt) || (loop.cond !== null && !scanExpr(loop.cond)) || - (loop.update !== null && !scanStmt(loop.update)) + (loop.kind === "for" && loop.update !== null && !scanStmt(loop.update)) ) return null; // A body temp stands for its initializer only when it is a constant @@ -337,6 +358,44 @@ export function matchBytesLoopVersion( return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); }, 0); + const whileAccesses = loop.kind === "while" ? whileCounterAccesses(loop) : null; + function whileCounterAccesses(loop: IrStmt & { kind: "while" }) { + return findWhileCounterAccesses(loop, { + counterLocal: (id) => { + const local = facts.locals.get(id); + return ( + local?.type.kind === "f64" && + !local.boxed && + !local.tdz && + !facts.captured.has(id) && + facts.initialized.has(id) + ); + }, + boundKey: (e) => { + if (e.type.kind !== "f64") return null; + // The guard evaluates the bound before the loop: a typed array's + // length needs the binding itself, not a temp the body declares. + if ( + e.kind === "bytesIntrinsic" && + e.method === "length" && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "bytes" && + invariant(e.receiver.localId) + ) + return `bytes-length:${e.receiver.localId}`; + if ( + e.kind === "strIntrinsic" && + e.method === "length" && + e.args.length === 0 && + e.receiver.kind === "varRef" && + e.receiver.type.kind === "string" && + invariant(e.receiver.localId) + ) + return `string-length:${e.receiver.localId}`; + return pureKey(e); + }, + }); + } const receivers: { ref: VarRef; global: boolean }[] = []; const receiverIndex = new Map(); const fixed = new Map(); @@ -356,6 +415,11 @@ export function matchBytesLoopVersion( // Worker programs: accesses through invariant receivers, proven or not, // become thread-owned when the guard finds no shared storage. const owned = new Map(); + // While loops: accesses at a counter below a bound, by receiver and bound. + const boundedChecks = new Map< + string, + { check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] } + >(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; @@ -390,11 +454,30 @@ export function matchBytesLoopVersion( }); return; } + const bounded = whileAccesses?.accesses.get(node); + const loopCounter = bounded && whileAccesses!.counters.get(bounded.counter); + if (bounded && loopCounter && (!loopCounter.wraps || loopCounter.bounds !== null)) { + const receiver = receiverOf(ref); + const key = `${receiver}|${bounded.key}|${bounded.adj}`; + const slack = bounded.offset + width - 1 + bounded.adj; + const group = boundedChecks.get(key); + if (group) { + group.check.slack = Math.max(group.check.slack, slack); + group.nodes.push({ node, access: bounded }); + } else + boundedChecks.set(key, { + check: { receiver, bound: bounded.bound, slack }, + nodes: [{ node, access: bounded }], + }); + return; + } const range = facts.ranges.get(index); if (!range) return; const end = range.max + width - 1; - // Only a length a buffer can have: an outer loop must not pay for a - // guard that bounds an inner loop's unbounded counter. + // Only an end below 2^31: an outer loop must not pay for a guard that + // bounds an inner loop's unbounded counter, and the counter of a loop to + // an array's length reaches 2^32 - 2, a length no typed array in + // practice has. if (range.min < 0 || end >= MAX_FIXED_END) return; const receiver = receiverOf(ref); const group = fixed.get(receiver); @@ -416,6 +499,7 @@ export function matchBytesLoopVersion( return everyStmtChild(s, visitExpr, visitStmt); }; loop.body.every(visitStmt); + if (loop.kind === "while") visitExpr(loop.cond); // Bound the counter so every counter operand stays a safe integer. let bound = SAFE; const affineGroups = [...affineChecks.values()].filter((group) => { @@ -431,8 +515,18 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); + const boundedKept = [...boundedChecks.values()].slice( + 0, + Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length), + ); const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); - if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; + if ( + fixedKept.length === 0 && + affineKept.length === 0 && + boundedKept.length === 0 && + ownedKept.length === 0 + ) + return null; const bounds = new Set(); const ownedNodes = new Set(); const used = new Set(); @@ -477,6 +571,39 @@ export function matchBytesLoopVersion( for (const group of affineKept) for (const term of group.check.terms) if (guardedRanges.has(term.expr)) guarded.add(term.key); for (const [node, key] of guardedNodes) if (guarded.has(key)) ranges.set(node, GUARDED_TERM); + // While loop counters of the kept checks: each entry value is an exact + // integer that no access offset takes below 0. In the fast copy each + // access's index is below its length, and its counter read at least the + // most negative offset. + const narrow = (e: IrExpr, range: IntegerRange): void => { + const known = ranges.get(e) ?? facts.ranges.get(e); + ranges.set( + e, + known ? { min: Math.max(known.min, range.min), max: Math.min(known.max, range.max) } : range, + ); + }; + const counterMins = new Map(); + for (const group of boundedKept) { + used.add(group.check.receiver); + for (const { node, access } of group.nodes) { + bounds.add(node); + const min = 0 - access.offset; + counterMins.set(access.counter, Math.max(counterMins.get(access.counter) ?? -SAFE, min)); + narrow(access.ref, { min: -MAX_OFFSET, max: SAFE }); + if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE }); + } + } + const counterChecks: CounterCheck[] = []; + for (const [id, min] of counterMins) { + const info = whileAccesses!.counters.get(id)!; + counterChecks.push({ + ref: { kind: "varRef", localId: id, type: F64, loc: loop.loc }, + min, + wrapBounds: info.wraps + ? info.bounds!.map((b) => ({ bound: b.bound, limit: 2 ** 31 - info.sum - b.adj })) + : [], + }); + } // Renumber the receivers the kept checks use. const order = [...used].sort((a, b) => a - b); const renumber = new Map(order.map((old, index) => [old, index])); @@ -500,6 +627,12 @@ export function matchBytesLoopVersion( minOffset: g.check.minOffset, maxEnd: g.check.maxEnd, })), + bounded: boundedKept.map((g) => ({ + receiver: renumber.get(g.check.receiver)!, + bound: g.check.bound, + slack: g.check.slack, + })), + counters: counterChecks, guarded, counted: affineKept.length > 0 ? counted : null, counterBound: needsBound ? bound : null, @@ -507,6 +640,307 @@ export function matchBytesLoopVersion( }; } +/** A typed-array access at a while loop counter: `counter + offset`, read + * or written while `counter < bound` (`<=` when `adj` is 1) holds. */ +interface WhileAccess { + counter: string; + /** The index, and the counter's read in it. */ + index: IrExpr; + ref: IrExpr; + bound: IrExpr; + key: string; + adj: number; + offset: number; +} + +interface WhileCounter { + /** Some increment is `c = (c + k) | 0`. */ + wraps: boolean; + /** The sum of every increment literal in the loop. */ + sum: number; + /** One bound from the condition of the loop and of every nested while + * loop that advances the counter; null when one of them has none. */ + bounds: { bound: IrExpr; adj: number }[] | null; +} + +/** `counter = counter + k`, `counter = (counter + k) | 0` or `counter++`. */ +function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | null { + if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.op === "+") + return { id: s.expr.localId, k: 1, wraps: false }; + if (s.kind !== "assign") return null; + let value = s.value, + wraps = false; + if ( + value.kind === "bin" && + value.op === "|" && + value.right.kind === "numLit" && + value.right.value === 0 && + value.left.kind === "bin" + ) { + value = value.left; + wraps = true; + } + if ( + value.kind !== "bin" || + value.op !== "+" || + value.left.kind !== "varRef" || + value.left.localId !== s.localId || + value.right.kind !== "numLit" || + !Number.isSafeInteger(value.right.value) || + value.right.value <= 0 || + value.right.value > MAX_OFFSET + ) + return null; + 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 `<=`). */ +function findWhileCounterAccesses( + loop: IrStmt & { kind: "while" }, + host: { + counterLocal: (id: string) => boolean; + /** A key for a bound expression whose value the loop never changes. */ + boundKey: (e: IrExpr) => string | null; + }, +): { accesses: Map; counters: Map } { + const steps = new Map(); + const refused = new Set(); + // Increments count only as statements of the loop's straight-line + // structure; any other write refuses the binding. + const writes = (list: readonly IrStmt[], direct: boolean): void => { + for (const s of list) { + const step = counterStep(s); + if (step && direct) { + const prior = steps.get(step.id); + steps.set(step.id, { + sum: (prior?.sum ?? 0) + step.k, + wraps: (prior?.wraps ?? false) || step.wraps, + }); + continue; + } + if (step) refused.add(step.id); + const nested = + direct && + (s.kind === "if" || s.kind === "while" || (s.kind === "block" && !s.labels?.length)); + if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") refused.add(s.localId); + if (s.kind === "tryCatch" && s.catchLocalId) refused.add(s.catchLocalId); + everyStmtChild( + s, + (e) => (exprWrites(e), true), + (c) => (writes([c], nested), true), + ); + } + }; + const exprWrites = (e: IrExpr): void => { + if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId); + everyExprChild( + e, + (c) => (exprWrites(c), true), + (c) => (writes([c], false), true), + ); + }; + exprWrites(loop.cond); + writes(loop.body, true); + const counter = (id: string): boolean => + steps.has(id) && !refused.has(id) && host.counterLocal(id); + + type Fact = { bound: IrExpr; key: string; adj: number }; + type Clean = ReadonlyMap; + // The bounds a condition puts on counters while it holds. + const facts = (cond: IrExpr, into: Map): void => { + if (cond.kind === "toBool") return facts(cond.operand, into); + if (cond.kind === "logical" && cond.op === "&&") { + facts(cond.left, into); + facts(cond.right, into); + return; + } + if (cond.kind !== "bin") return; + const below = cond.op === "<" || cond.op === "<=", + above = cond.op === ">" || cond.op === ">="; + const c = below ? cond.left : above ? cond.right : null, + bound = below ? cond.right : cond.left; + if (c?.kind !== "varRef" || !counter(c.localId) || into.has(c.localId)) return; + const key = host.boundKey(bound); + if (key === null) return; + into.set(c.localId, { bound, key, adj: cond.op === "<=" || cond.op === ">=" ? 1 : 0 }); + }; + // The facts that hold after either of two paths. + const meet = (a: Clean | null, b: Clean | null): Clean | null => { + if (a === null) return b; + if (b === null) return a; + const out = new Map(); + for (const [id, fact] of a) if (b.get(id) === fact) out.set(id, fact); + return out; + }; + const withFacts = (clean: Clean, cond: IrExpr): Clean => { + const out = new Map(clean); + facts(cond, out); + return out; + }; + const accesses = new Map(); + const record = (node: Node, index: IrExpr, clean: Clean): void => { + let ref: IrExpr = index, + offset = 0; + if (index.kind === "bin" && (index.op === "+" || index.op === "-")) { + const literal = + index.right.kind === "numLit" ? index.right : index.op === "+" ? index.left : null; + const other = literal === index.right ? index.left : index.right; + if (literal?.kind !== "numLit" || !Number.isSafeInteger(literal.value)) return; + if (Math.abs(literal.value) > MAX_OFFSET || Object.is(literal.value, -0)) return; + offset = index.op === "-" ? -literal.value : literal.value; + ref = other; + } + if (ref.kind !== "varRef") return; + const fact = clean.get(ref.localId); + if (fact) accesses.set(node, { counter: ref.localId, index, ref, ...fact, offset }); + }; + const expr = (e: IrExpr, clean: Clean): void => { + if (e.kind === "bytesIntrinsic") { + const numeric = byteNumberAccess(e); + const index = numeric + ? e.args[numeric.offsetArg] + : e.method === "get" + ? e.args[0] + : undefined; + if (index) record(e, index, clean); + } + if (e.kind === "logical" && e.op === "&&") { + expr(e.left, clean); + expr(e.right, withFacts(clean, e.left)); + return; + } + if (e.kind === "ternary") { + expr(e.cond, clean); + expr(e.then, withFacts(clean, e.cond)); + expr(e.else_, clean); + return; + } + everyExprChild( + e, + (c) => (expr(c, clean), true), + (c) => (stmts([c], clean), true), + ); + }; + // Walks statements in order; the result is the clean state where the + // list falls through, or null when it never does. + const stmts = (list: readonly IrStmt[], clean: Clean): Clean | null => { + let current: Clean | null = clean; + for (const s of list) { + if (current === null) return null; + const step = counterStep(s); + if (step) { + const next: Map = new Map(current); + next.delete(step.id); + current = next; + continue; + } + switch (s.kind) { + case "if": { + expr(s.cond, current); + const then = stmts(s.then, withFacts(current, s.cond)); + const otherwise = stmts(s.else_ ?? [], current); + current = meet(then, otherwise); + break; + } + case "while": { + // A pass of the nested loop may follow its own increments. + const advanced: Map = new Map(current); + for (const id of writtenCounters(s.body)) advanced.delete(id); + expr(s.cond, advanced); + stmts(s.body, withFacts(advanced, s.cond)); + current = advanced; + break; + } + case "block": + if (!s.labels?.length) { + current = stmts(s.body, current); + break; + } + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "bytesSet": + record(s, s.index, current); + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + case "break": + case "continue": + case "return": + case "throw": + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + current = null; + break; + default: + everyStmtChild( + s, + (e) => (expr(e, current!), true), + (c) => (stmts([c], current!), true), + ); + break; + } + } + return current; + }; + const writtenCounters = (list: readonly IrStmt[]): Set => { + const out = new Set(); + const visit = (s: IrStmt): boolean => { + const step = counterStep(s); + if (step) out.add(step.id); + return everyStmtChild(s, () => true, visit); + }; + list.every(visit); + return out; + }; + expr(loop.cond, new Map()); + stmts(loop.body, withFacts(new Map(), loop.cond)); + + // Each counter's bounds: the loop's and those of nested while loops that + // advance it. + const counters = new Map(); + const loops: (IrStmt & { kind: "while" })[] = [loop]; + const collectLoops = (s: IrStmt): boolean => { + if (s.kind === "while") loops.push(s); + return everyStmtChild(s, () => true, collectLoops); + }; + loop.body.every(collectLoops); + for (const [id, step] of steps) { + if (!counter(id)) continue; + let bounds: WhileCounter["bounds"] = []; + for (const l of loops) { + if (l !== loop && !writtenCounters(l.body).has(id)) continue; + const fact = withFacts(new Map(), l.cond).get(id); + if (!fact) { + bounds = null; + break; + } + bounds.push({ bound: fact.bound, adj: fact.adj }); + } + counters.set(id, { ...step, bounds }); + } + return { accesses, counters }; +} + /** Branches to `fast` when every access the version proves is in bounds for * every iteration and every view it proves thread-owned has no shared * storage, and to `slow` otherwise (including a counted loop whose @@ -553,6 +987,27 @@ export function emitBytesLoopVersionGuard( B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); conditions.push(ok); } + for (const counter of version.counters) { + const value = host.emitExpr(counter.ref).name; + conditions.push(emitExactIntegerTest(host, value, counter.min, SAFE).ok); + for (const wrap of counter.wrapBounds) { + const ok = B.tmp(); + B.line(`${ok} = fcmp ole double ${host.emitExpr(wrap.bound).name}, ${f64Lit(wrap.limit)}`); + conditions.push(ok); + } + } + for (const check of version.bounded) { + // `bound <= length - slack` in doubles: both sides are exact (a length + // is below 2^53 and a slack below 2^32), and NaN fails. + const bound = host.emitExpr(check.bound).name; + const length = B.tmp(), + room = B.tmp(), + ok = B.tmp(); + B.line(`${length} = uitofp i64 ${lengths[check.receiver]} to double`); + B.line(`${room} = fsub double ${length}, ${f64Lit(check.slack)}`); + B.line(`${ok} = fcmp ole double ${bound}, ${room}`); + conditions.push(ok); + } if (counted && version.affine.length > 0) { // The first and last counter values, in ascending order. The last value // of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ * @@ -599,7 +1054,7 @@ export function emitBytesLoopVersionGuard( for (const term of check.terms) { const value = host.emitExpr(term.expr); if (version.guarded.has(term.key)) { - const tested = emitGuardedTerm(host, value.name); + const tested = emitExactIntegerTest(host, value.name, GUARDED_TERM.min, GUARDED_TERM.max); conditions.push(tested.ok); const sum = B.tmp(); B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`); @@ -653,10 +1108,15 @@ export function emitBytesLoopVersionGuard( else B.condBr(valid, fast, slow); } -/** Tests that a guarded term is an exact integer within GUARDED_TERM (not - * -0; NaN fails the ordered comparisons) and gives its i64. The integer is - * frozen, so it is a fixed value even when the test fails. */ -function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; integer: string } { +/** Tests that a double is an exact integer within [min, max] (not -0; NaN + * fails the ordered comparisons) and gives its i64. The integer is frozen, + * so it is a fixed value even when the test fails. */ +function emitExactIntegerTest( + host: LlvmEmitterContext, + value: string, + min: number, + max: number, +): { ok: string; integer: string } { const B = host.B; const low = B.tmp(), high = B.tmp(), @@ -668,8 +1128,8 @@ function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; backBits = B.tmp(), exact = B.tmp(), ok = B.tmp(); - B.line(`${low} = fcmp oge double ${value}, ${f64Lit(GUARDED_TERM.min)}`); - B.line(`${high} = fcmp ole double ${value}, ${f64Lit(GUARDED_TERM.max)}`); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); B.line(`${inRange} = and i1 ${low}, ${high}`); B.line(`${raw} = fptosi double ${value} to i64`); B.line(`${integer} = freeze i64 ${raw}`); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index a22de3445..4b3550756 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -51,7 +51,7 @@ import { emitCountedLoopSafety, emitPairedLoopSafety, } from "./counted-loops.js"; -import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; +import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; @@ -795,6 +795,16 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "while": { + const version = + emitter.bytesLoopVersionsEnabled && + emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && + !emitter.bytesLoopVersioning.has(s) + ? emitter.matchBytesLoopVersion(s, null) + : null; + if (version) { + emitVersionedLoop(emitter, s, version); + break; + } const workerBudget = emitter.workerLoopBudget(s); const le = B.newLabel("loop.e"); emitter.emitLoopCopies(s, true, le, (exit) => { @@ -981,35 +991,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop; const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; if (version) { - const fast = B.newLabel("bytes.version.fast"), - slow = B.newLabel("bytes.version.slow"), - done = B.newLabel("bytes.version.done"); - emitBytesLoopVersionGuard(emitter, version, fast, slow); - const originalBounds = emitter.bytesBounds, - originalOwned = emitter.bytesOwned, - originalRanges = emitter.integerRanges; - emitter.bytesLoopVersioning.add(s); - try { - B.startBlock(slow); - emitter.emitStmt(s); - B.br(done); - B.startBlock(fast); - emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); - if (version.owned.size > 0) - emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); - if (version.ranges.size > 0) - emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) emitter.provenCountedGuards.add(s); - emitter.emitStmt(s); - B.br(done); - } finally { - emitter.bytesBounds = originalBounds; - emitter.bytesOwned = originalOwned; - emitter.integerRanges = originalRanges; - emitter.provenCountedGuards.delete(s); - emitter.bytesLoopVersioning.delete(s); - } - B.startBlock(done); + emitVersionedLoop(emitter, s, version); break; } let countedJoin: string | null = null; @@ -1896,6 +1878,45 @@ const SPECULATION_BAILS = 8; * when speculative-integers.ts plans a fast integer copy. Both copies * share the loop's setup and exit; the ordinary copy comes first so the * fast copy is only emitted when every resume point exists. */ +/** A typed-array loop version: the guard, then the checked copy, which + * runs whenever the guard fails, and the copy whose accesses the guard + * proves in bounds (or thread-owned). */ +function emitVersionedLoop( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" }, + version: BytesLoopVersion, +): void { + const B = emitter.B; + const fast = B.newLabel("bytes.version.fast"), + slow = B.newLabel("bytes.version.slow"), + done = B.newLabel("bytes.version.done"); + emitBytesLoopVersionGuard(emitter, version, fast, slow); + const originalBounds = emitter.bytesBounds, + originalOwned = emitter.bytesOwned, + originalRanges = emitter.integerRanges; + emitter.bytesLoopVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); + if (version.ranges.size > 0) + emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); + if (version.counted?.guarded) emitter.provenCountedGuards.add(s); + emitter.emitStmt(s); + B.br(done); + } finally { + emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; + emitter.integerRanges = originalRanges; + emitter.provenCountedGuards.delete(s); + emitter.bytesLoopVersioning.delete(s); + } + B.startBlock(done); +} + export function emitLoopCopies( emitter: LlEmitter, loop: Extract, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c1cf66af4..e4e37ec4f 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3843,7 +3843,7 @@ export class LlEmitter { /** A binding's storage: a module global, a plain local slot, or a boxed * local (the slot holds the capture BOX; access goes through it). */ matchBytesLoopVersion( - s: IrStmt & { kind: "for" }, + s: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, ): ReturnType { const fn = this.bytesLoopFunction; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..f5cfba0cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6998-typed-array-while-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6998-typed-array-while-loop-versions.ts b/tests/corpus/6998-typed-array-while-loop-versions.ts new file mode 100644 index 000000000..383a2437f --- /dev/null +++ b/tests/corpus/6998-typed-array-while-loop-versions.ts @@ -0,0 +1,149 @@ +// While loops that scan typed arrays with a counter below a bound: scanners +// with nested loops and `&&` conditions, `| 0` increments, look-behind and +// look-ahead reads, writes, DataView reads, string-length bounds, and +// bounds or starts past the array, fractional, negative, -0 or NaN. + +function words(bytes: Uint8Array, end: number): number { + let pos = 0; + let count = 0; + while (pos < end) { + while (pos < end && (bytes[pos] === 32 || bytes[pos] === 10)) pos++; + if (pos < end) count++; + while (pos < end && bytes[pos] !== 32 && bytes[pos] !== 10) pos++; + } + return count; +} + +function identifiers(chars: Uint16Array, start: number, end: number): string { + let pos = start; + let count = 0; + let mix = 0; + while (pos < end) { + let hash = 0x811c9dc5 | 0; + while (pos < end) { + const c = chars[pos]!; + if (!((c >= 97 && c <= 122) || c === 95)) break; + hash = Math.imul(hash ^ c, 0x01000193); + pos = (pos + 1) | 0; + } + mix = (mix + hash) | 0; + count++; + pos = (pos + 1) | 0; + } + return `${count} ${mix >>> 0} ${pos}`; +} + +function around(values: Int16Array, start: number, end: number): number[] { + const out: number[] = []; + let i = start; + while (i <= end) { + out.push(values[i - 1]! + values[i + 2]! * 2); + i += 1; + } + return out; +} + +function fill(out: Float32Array, end: number, value: number): number { + let i = 0; + while (i < end) { + out[i] = value + i; + if (i < end - 1 && out[i] > 2) { + i += 2; + continue; + } + i++; + } + return i; +} + +function afterInner(bytes: Uint8Array): number { + let pos = 0; + let sum = 0; + while (pos < bytes.length) { + while (pos < bytes.length && bytes[pos] !== 0) pos++; + // The inner loop may stop at the length: this read stays checked. + sum += bytes[pos] ?? -1; + pos++; + } + return sum; +} + +function firstAbove(samples: Float64Array, limit: number): number { + let i = 0; + while (i < samples.length) { + if (samples[i]! > limit) return i; + i = i + 1; + } + return -1; +} + +function halves(view: DataView, end: number): number { + let at = 0; + let total = 0; + while (at < end) { + total += view.getUint16(at, true); + at += 2; + } + return total; +} + +function letters(text: string, codes: Uint8Array): number { + let i = 0; + let sum = 0; + while (i < text.length) { + sum += codes[i]! * (text.charCodeAt(i) & 7); + i++; + } + return sum; +} + +function notCounters(bytes: Uint8Array): string { + let i = 1; + let doubled = 0; + while (i < bytes.length) { + doubled += bytes[i]!; + i = i * 2; + } + let j = bytes.length - 1; + let down = 0; + while (j < bytes.length && j >= 0) { + down += bytes[j]!; + j--; + } + return `${doubled} ${down}`; +} + +const text = "let alpha = beta_gamma\nreturn delta "; +const bytes = new Uint8Array(text.length); +const chars = new Uint16Array(text.length); +for (let i = 0; i < text.length; i++) { + bytes[i] = text.charCodeAt(i); + chars[i] = text.charCodeAt(i); +} +console.log("words", words(bytes, bytes.length), words(bytes, 10), words(bytes, bytes.length + 5), words(bytes, 7.5), words(bytes, NaN)); +console.log("identifiers", identifiers(chars, 0, chars.length)); +console.log("identifiers past end", identifiers(chars, 4, chars.length + 3)); +console.log("identifiers starts", identifiers(chars, -2, 9), identifiers(chars, 2.5, 12), identifiers(chars, -0, 6)); + +const values = new Int16Array([5, -6, 7, -8, 9, -10, 11]); +console.log("around", around(values, 1, 4).join(","), around(values, 0, 6).join(","), around(values, -1, 1).join(",")); + +const floats = new Float32Array(6); +console.log("fill", fill(floats, 6, 0.5), floats.join(",")); +const short = new Float32Array(3); +console.log("fill past end", fill(short, 5, 1), short.join(",")); + +console.log("after inner", afterInner(new Uint8Array([1, 2, 0, 3, 0, 4, 5]))); +console.log("first above", firstAbove(new Float64Array([0.5, 1.5, 2.5]), 1), firstAbove(new Float64Array([0.5]), 1)); + +const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const view = new DataView(buffer.buffer); +console.log("halves", halves(view, 8)); +try { + console.log("halves past end", halves(view, 10)); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} + +console.log("letters", letters("abcdef", new Uint8Array([1, 2, 3, 4, 5, 6])), letters("abcdefgh", new Uint8Array([1, 2, 3]))); +console.log("not counters", notCounters(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))); diff --git a/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json new file mode 100644 index 000000000..204a01a83 --- /dev/null +++ b/tests/harness/corpus-costs/6998-typed-array-while-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 5000 } From 9aa3b15174dc524eb1157429e133710a9b54a547 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:44:27 -0500 Subject: [PATCH 037/191] 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 038/191] 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 c9ae4d80196eeabdacc7574f3dd216bf0422f32e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:55:30 -0500 Subject: [PATCH 039/191] Look up spawnSync commands on the replacement environment's PATH With an `env` option, spawnSync searched the parent's PATH for a command without a '/'. Node (libuv) searches the PATH of the child's environment, or the default path when that environment has none, so a command only on the child's PATH ran as ENOENT and one only on the parent's PATH ran where Node reports ENOENT. The lookup now follows libuv: each PATH entry in order, an empty entry as the current directory, and EACCES when a candidate exists but cannot run. --- packages/runtime/src/scr_child.c | 62 +++++++++++++++++++++++++++-- tests/corpus/7030-spawn-sync-env.ts | 19 +++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 387885c42..df60fcb26 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2907,6 +2907,7 @@ void scr_children_poll(void) { #include #include #include +#include #include #include #include @@ -3111,8 +3112,55 @@ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status, static char **scr_exec_envp(ScrArr *pairs); static void scr_exec_envp_free(char **envp); +/* The executable `file` names when `envp` replaces the child's + * environment, found as libuv finds it: a name with a '/' is used as + * given; otherwise each entry of the replacement's PATH (an empty entry + * is the current directory), or of the default path when the replacement + * has no PATH, is tried in order. Returns a malloc'd path, or NULL with + * *err set: EACCES when a candidate exists but cannot run, else ENOENT. */ +static char *scr_spawn_path_lookup(const char *file, char *const *envp, int *err) { + if (strchr(file, '/')) { + char *copy = strdup(file); + if (!copy) scr_child_oom(); + return copy; + } + const char *path = "/usr/bin:/bin"; + for (char *const *e = envp; *e; e++) { + if (strncmp(*e, "PATH=", 5) == 0) { + path = *e + 5; + break; + } + } + size_t file_len = strlen(file); + bool denied = false; + for (const char *dir = path;;) { + const char *end = strchr(dir, ':'); + size_t dir_len = end ? (size_t)(end - dir) : strlen(dir); + const char *prefix = dir_len ? dir : "."; + size_t prefix_len = dir_len ? dir_len : 1; + char *candidate = malloc(prefix_len + 1 + file_len + 1); + if (!candidate) scr_child_oom(); + memcpy(candidate, prefix, prefix_len); + candidate[prefix_len] = '/'; + memcpy(candidate + prefix_len + 1, file, file_len + 1); + struct stat st; + if (stat(candidate, &st) == 0) { + if (S_ISREG(st.st_mode) && access(candidate, X_OK) == 0) return candidate; + denied = true; + } else if (errno == EACCES) { + denied = true; + } + free(candidate); + if (!end) break; + dir = end + 1; + } + *err = denied ? EACCES : ENOENT; + return NULL; +} + /* env_pairs: NULL inherits the parent's environment; else [k,v,...] - * replaces it, as Node's `env` option does. */ + * replaces it, as Node's `env` option does, and the command is looked up + * on the replacement's PATH (scr_spawn_path_lookup). */ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, int err_mode, ScrArr *env_pairs) { @@ -3152,8 +3200,16 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou char **argv = scr_child_argv(cmd, args); char **envp = env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, - envp != NULL ? envp : environ); + int spawn_err; + if (envp != NULL) { + char *resolved = scr_spawn_path_lookup(cmd->data, envp, &spawn_err); + if (resolved) { + spawn_err = posix_spawn(&pid, resolved, &fa, NULL, argv, envp); + free(resolved); + } + } else { + spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ); + } posix_spawn_file_actions_destroy(&fa); free(argv); scr_exec_envp_free(envp); diff --git a/tests/corpus/7030-spawn-sync-env.ts b/tests/corpus/7030-spawn-sync-env.ts index e613c81f0..0ab001b8f 100644 --- a/tests/corpus/7030-spawn-sync-env.ts +++ b/tests/corpus/7030-spawn-sync-env.ts @@ -1,6 +1,9 @@ // spawnSync's env option replaces the child's environment, alone or with // the other options. import { spawnSync } from "node:child_process"; +import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; const merged = spawnSync("sh", ["-c", 'echo "$GREETING $HOME_SET"'], { env: { ...process.env, GREETING: "hello", HOME_SET: process.env.HOME ? "yes" : "no" }, @@ -26,3 +29,19 @@ console.log(quiet.status, quiet.stderr.toString().trim(), quiet.stdout.toString( const missing = spawnSync("/nonexistent/tool", [], { env: { A: "1" } }); console.log(missing.status, missing.error?.message); + +// The command is found on the replacement environment's PATH, or on the +// default path when the replacement has none, not on the parent's PATH. +const dir = mkdtempSync(join(tmpdir(), "scr-spawn-path-")); +writeFileSync(join(dir, "child-only-tool"), "#!/bin/sh\necho child-only\n"); +chmodSync(join(dir, "child-only-tool"), 0o755); +writeFileSync(join(dir, "not-executable"), "#!/bin/sh\necho never\n"); +const viaPath = spawnSync("child-only-tool", [], { env: { PATH: `/nonexistent:${dir}` }, encoding: "utf8" }); +console.log(viaPath.status, viaPath.stdout.trim()); +const defaultPath = spawnSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }); +console.log(defaultPath.status, defaultPath.stdout.trim()); +const notOnPath = spawnSync("sh", ["-c", "echo never"], { env: { PATH: dir }, encoding: "utf8" }); +console.log(notOnPath.status, notOnPath.error?.message); +const denied = spawnSync("not-executable", [], { env: { PATH: dir }, encoding: "utf8" }); +console.log(denied.status, denied.error?.message); +rmSync(dir, { recursive: true }); From 05acb4d24783f63cabf49cdc177d138fa5ea0f70 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:58:47 -0500 Subject: [PATCH 040/191] Update the DiffieHellman runtime test for the prime search test_dh.c includes scr_dh.c and still called sd_generate_safe_prime without its new top_two argument, so dh.test.ts failed to compile. It now passes false, as the DiffieHellman paths do, and a new case checks generatePrime's search: the sizes and add forms with one possible result, top-two-bit safe-prime starts, and checks above OpenSSL's round count. --- packages/runtime/test/test_dh.c | 54 ++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/runtime/test/test_dh.c b/packages/runtime/test/test_dh.c index aa8f175a3..f459f1047 100644 --- a/packages/runtime/test/test_dh.c +++ b/packages/runtime/test/test_dh.c @@ -199,7 +199,7 @@ static void test_generate(void) { mbedtls_mpi_lset(&g, 2); for (size_t i = 0; i < sizeof adds / sizeof adds[0]; i++) { for (int trial = 0; trial < 2; trial++) { - bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1); + bool ok = sd_generate_safe_prime(&p, 512, adds[i], adds[i] - 1, false); mbedtls_mpi_uint r = 0; ok = ok && mbedtls_mpi_bitlen(&p) == 512 && mbedtls_mpi_mod_int(&r, &p, adds[i]) == 0 && r == adds[i] - 1; mbedtls_mpi_copy(&q, &p); @@ -217,12 +217,64 @@ static void test_generate(void) { mbedtls_mpi_free(&g); } +/* generatePrime's search: the sizes and add forms with one possible + * result, top-two-bit safe-prime starts, and checks above OpenSSL's + * round count. */ +static void test_generate_prime(void) { + static const struct { + size_t bits; + bool safe; + int add, rem; + int want; + } cases[] = { + {2, false, 0, 0, 3}, {3, false, 0, 0, 7}, {4, false, 0, 0, 13}, {3, true, 0, 0, 7}, + {6, true, 0, 0, 59}, {7, true, 0, 0, 107}, {8, false, 200, 1, 401}, {8, false, 128, 0, 257}, + {5, false, 16, 3, 19}, + }; + mbedtls_mpi p, add, rem, q; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&add); + mbedtls_mpi_init(&rem); + mbedtls_mpi_init(&q); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + mbedtls_mpi_lset(&add, cases[i].add); + mbedtls_mpi_lset(&rem, cases[i].rem); + bool ok = sd_generate_prime(&p, cases[i].bits, cases[i].safe, cases[i].add ? &add : NULL, + cases[i].rem ? &rem : NULL) && + mbedtls_mpi_cmp_int(&p, cases[i].want) == 0; + char what[96]; + snprintf(what, sizeof what, "generatePrime(%zu, safe %d, add %d, rem %d) is %d", cases[i].bits, cases[i].safe, + cases[i].add, cases[i].rem, cases[i].want); + check(ok, what); + } + for (int trial = 0; trial < 2; trial++) { + bool ok = sd_generate_safe_prime(&p, 128, 12, 11, true) && mbedtls_mpi_bitlen(&p) == 128 && + mbedtls_mpi_get_bit(&p, 126) == 1; + mbedtls_mpi_copy(&q, &p); + mbedtls_mpi_shift_r(&q, 1); + ok = ok && mbedtls_mpi_is_prime_ext(&p, 40, sd_random, NULL) == 0 && + mbedtls_mpi_is_prime_ext(&q, 40, sd_random, NULL) == 0; + check(ok, "a 128-bit safe prime with its top two bits set"); + } + ScDhSmallPrimes *small = sd_small_primes(); + mbedtls_mpi_read_string(&q, 10, "3215031751"); + check(!sd_is_prime_checks(small, &q, 200), "200 rounds reject a strong pseudoprime to 2, 3, 5 and 7"); + mbedtls_mpi_read_string(&q, 10, "2305843009213693951"); + check(sd_is_prime_checks(small, &q, 200), "200 rounds accept 2^61 - 1"); + free(small); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&add); + mbedtls_mpi_free(&rem); + mbedtls_mpi_free(&q); +} + int main(void) { test_mont_pow(); test_is_prime(); test_check_params(); test_long_exponent(); test_generate(); + test_generate_prime(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed ? 1 : 0; } From c43d4002e71f3c87a8b7bee79c12c02793bf1c2b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:59:57 -0500 Subject: [PATCH 041/191] Skip only always-throwing calls as record producers in the key-order witness The witness skipped every libCall whose signature result is VOID. VOID is also the signature of runtime calls that take their record type from the use site and build the record at run time, such as net.serverAddress (`server.address()`). A literal of the same shape then set the shape's key order, and the runtime record enumerated in the literal's order instead of Node's (`port, family, address` for `address, family, port`). The witness now skips only the calls in ALWAYS_THROW_LIB_FNS, which never produce a value. The set moves from validate.ts's result-type check to builtin-effects.ts, and both use it. --- .../lowering/key-order-witness.test.ts | 30 +++++++++++++++++++ .../frontend/lowering/key-order-witness.ts | 11 +++---- packages/compiler/src/ir/builtin-effects.ts | 22 ++++++++++++++ packages/compiler/src/ir/validate.ts | 29 ++---------------- 4 files changed, 61 insertions(+), 31 deletions(-) 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..8632bad47 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -157,6 +157,36 @@ test("a synthesized construction pins today's table", () => { expect(f.order(ab)).toEqual(["a", "b"]); }); +test("a runtime builder typed by its use site pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + // net.serverAddress's signature result is VOID: the call takes its record + // type from its use site, and the runtime builds the record in that order. + const built: IrExpr = { kind: "libCall", fn: "net.serverAddress", args: [], type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(built)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("an always-throwing call typed as the record leaves its literals' order", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [], type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(thrown)]); + expect(f.order(ab)).toEqual(["b", "a"]); +}); + test("a checked conversion alone pins today's table", () => { const f = fixture(); const ab = f.record( diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 7ce8ae074..4adfb87ab 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -45,7 +45,7 @@ import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; -import { LIB_FN_SIGS } from "../../ir/builtin-signatures.js"; +import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; /** One property of an object literal, in source order. */ export type KeyOrderSegment = @@ -313,10 +313,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio // Runtime surfaces (console, util.format, workers) that take a // record may walk its keys; runtime builders make new records. for (const arg of e.args) observedRoots.push(arg.type); - // A void runtime call typed by its use site is a throw standing in - // for a value (`error.nodeThrow` in a narrowing ternary): it never - // produces one. - if (LIB_FN_SIGS[e.fn].result.kind !== "void") producedBy(e.type, true); + // An always-throwing call stands in for a value (`error.nodeThrow` + // in a narrowing ternary) and never produces one. Other calls + // whose signature result is VOID are typed by their use site and + // build the record at run time (`server.address()`). + if (!ALWAYS_THROW_LIB_FNS.has(e.fn)) producedBy(e.type, true); return; case "intrinsic": // Intrinsics hand back values built elsewhere, except allSettled's diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..c2194a13c 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1001,3 +1001,25 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.createSign", "crypto.createVerify", ]); + +/** libCall members that always throw. Their result type is the replaced + * expression's own and is never materialized: global.undefRead's is the + * declared type of the undefined global; the others stand in for a value + * of the expression they replace. The fs Chk ladders qualify: every + * validation failure throws Node's typed error, and a full pass throws the + * trailing compiler-rendered fence. */ +export const ALWAYS_THROW_LIB_FNS: ReadonlySet = new Set([ + "global.undefRead", + "error.nodeThrow", + "error.argTypeThrow", + "error.propTypeThrow", + "fs.mkdtempChk", + "fs.readFileChk", + "fs.opendirChk", + "fs.watchFileChk", + "fs.lchmodChk", + "fs.readChk", + "fs.streamOptsChk", + "net.connectOptsChk", + "tls.caCertsChk", +]); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..163c2a41c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -61,6 +61,7 @@ import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js"; +import { ALWAYS_THROW_LIB_FNS } from "./builtin-effects.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ const CRYPTO_PROMISE_VALUES: Readonly> = { @@ -5345,32 +5346,8 @@ function validateFunction( const callback = e.args[e.args.length - 1]?.type; if (callback?.kind !== "func") err(`libCall ${e.fn} callback must be a function`, e.loc); } - if (e.fn === "global.undefRead") { - // Always throws — the result type is whatever the declared type - // of the undefined global mapped to (never materialized). - return true; - } - if ( - e.fn === "error.nodeThrow" || - e.fn === "error.argTypeThrow" || - e.fn === "error.propTypeThrow" || - e.fn === "fs.mkdtempChk" || - e.fn === "fs.readFileChk" || - e.fn === "fs.opendirChk" || - e.fn === "fs.watchFileChk" || - e.fn === "fs.lchmodChk" || - e.fn === "fs.readChk" || - e.fn === "fs.streamOptsChk" || - e.fn === "net.connectOptsChk" || - e.fn === "tls.caCertsChk" - ) { - // Always throws — the result type is the replaced expression's - // own (never materialized; the global.undefRead pattern). The - // fs Chk ladders qualify: every validation failure throws - // Node's typed error, and a full pass throws the trailing - // compiler-rendered fence. - return true; - } + // Always throws: the result type is never materialized. + if (ALWAYS_THROW_LIB_FNS.has(e.fn)) return true; if (e.fn === "error.new" || e.fn === "error.newOptions") { // Which builtin the runtime constructs is named by the result type. if (!isBuiltinErrorObject(e.type)) { From e4dc0900d7f451963914422228bdebc18e4b1e20 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:09:08 -0500 Subject: [PATCH 042/191] Fence arguments.length in function expressions that call themselves by name A function expression stored in a function type with more parameters folds `arguments.length` to that type's parameter count, the count every call through the slot passes. A named expression that calls itself through its own name passes only its declared parameters there, so no constant answers both calls. Such a read now reports SC1090 with the reason, and expressions that do not refer to their own name keep the fold. --- .../lowering/expressions/property-access.ts | 38 ++++++++++++++++--- .../arguments-length-self-call.ts.json | 6 +++ .../diagnostics/arguments-length-self-call.ts | 8 ++++ .../arguments-length-self-call.ts.txt | 6 +++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json create mode 100644 tests/diagnostics/arguments-length-self-call.ts create mode 100644 tests/harness/__snapshots__/arguments-length-self-call.ts.txt diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 44d38acd4..5df3c7fe0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -669,12 +669,17 @@ export function lowerPropertyAccessExpr( const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( (p) => !isThisParameter(p), ).length; - return { - kind: "numLit", - value: Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0), - type: F64, - loc, - }; + const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + // Calls through the expression's own name pass the declared count, + // calls through the wider slot pass more: no constant answers both. + if (count > declared && referencesOwnName(lowerer, fn)) { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call)", + ); + } + return { kind: "numLit", value: count, type: F64, loc }; } lowerer.unsupported( "SC1090", @@ -4442,6 +4447,27 @@ function lowerPromiseConstructorRead( return null; } +/** Whether a named function expression refers to itself by name in its + * body (a recursive call, or the function escaping under its own name). */ +function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { + if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return false; + const self = lowerer.checker.getSymbolAtLocation(fn.name); + if (self === undefined) return false; + const name = fn.name.text; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (found) return "stop"; + if ( + ts.isIdentifier(node) && + node.text === name && + lowerer.checker.getSymbolAtLocation(node) === self + ) + found = true; + return undefined; + }); + return found; +} + /** The argument count every caller passes a function expression whose * value flows into one fixed-arity function type, an array method's * callback or an annotated slot: that type's parameter count. Null when diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json new file mode 100644 index 000000000..8ed5027e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/arguments-length-self-call.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/arguments-length-self-call.ts" + ], + "diags": [] +} diff --git a/tests/diagnostics/arguments-length-self-call.ts b/tests/diagnostics/arguments-length-self-call.ts new file mode 100644 index 000000000..3afc2cb33 --- /dev/null +++ b/tests/diagnostics/arguments-length-self-call.ts @@ -0,0 +1,8 @@ +// `arguments.length` in a function expression stored in a slot with more +// parameters counts that slot's arguments, but a call through the +// expression's own name passes only the declared ones: no constant answers +// both, so the read fences. +const count: (a: number, b: number) => number = function self(a: number): number { + return a > 0 ? self(a - 1) + arguments.length : arguments.length; +}; +console.log(count(2, 0)); diff --git a/tests/harness/__snapshots__/arguments-length-self-call.ts.txt b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt new file mode 100644 index 000000000..7e94b56c6 --- /dev/null +++ b/tests/harness/__snapshots__/arguments-length-self-call.ts.txt @@ -0,0 +1,6 @@ +arguments-length-self-call.ts:6:32 - error SC1090: 'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call) are not supported yet + + 5 | const count: (a: number, b: number) => number = function self(a: number): number { + 6 | return a > 0 ? self(a - 1) + arguments.length : arguments.length; + | ^~~~~~~~~~~~~~~~ + 7 | }; \ No newline at end of file From affe386bbf62de66812386b9fa07fbba1d8f37fd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:09:43 -0500 Subject: [PATCH 043/191] Throw for every object on the right of an arrow instanceof, and give blocked bodies their this - An arrow function or method on the right of an untyped `instanceof` has no prototype. Node throws "Function has non-object prototype 'undefined' in instanceof check" for any object, but the runtime threw only for plain objects, arrays and functions, and answered false for typed arrays, native handles, promises and typed references. It now answers false only for primitives. - Coverage lowers the body of a signature-blocked function for its diagnostics. A declared `this` parameter now gives that body its receiver, as lowerFunction does, so `this.label` reports nothing. Before, each use reported SC1080. --- .../src/frontend/lowering/lower-functions.ts | 3 +++ packages/runtime/src/scr_json.c | 10 +++++++--- tests/corpus/7005-dyn-constructor-instanceof/main.ts | 3 ++- .../node_modules/tag-types/index.d.ts | 1 + .../node_modules/tag-types/index.js | 10 ++++++++++ tests/coverage-fixtures/blocked-signature-body.ts | 4 ++++ tests/harness/coverage.test.ts | 12 ++++++++---- 7 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index ea7d7957b..c1cca9c64 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -893,6 +893,9 @@ export function blockedBodyDiagnostics( lowerer.fnStack.push(ctx); let internal = false; try { + // A declared `this` parameter gives the body its receiver, as + // lowerFunction declares it. + if (decl.parameters.nodes.some(isThisParameter)) declareCallReceiver(lowerer, decl, false); for (const param of decl.parameters.nodes) { if (isThisParameter(param)) continue; const declaredParam = lowersCleanly(lowerer, () => { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 906be0ccd..fbfcade66 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3968,10 +3968,14 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc bool undefined_prototype = prototype->kind == SCR_DYN_UNDEF; scr_dyn_release(prototype); /* A function the program compiled with no prototype (an arrow function - * or a method): Node's TypeError for an object, false for a primitive. - * The runtime's own constructors carry "native:" signatures. */ + * or a method): false for a primitive, and Node's TypeError for every + * object, typed arrays, native handles, promises and typed references + * included. The runtime's own constructors carry "native:" signatures. */ if (undefined_prototype && (!callee->v.fn.sig || strncmp(callee->v.fn.sig, "native:", 7) != 0)) { - if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_ARR && value->kind != SCR_DYN_FUNC) return false; + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_BOOL || + value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BIGINT || + value->kind == SCR_DYN_SYMBOL) + return false; static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/main.ts b/tests/corpus/7005-dyn-constructor-instanceof/main.ts index c32a820c2..21fcbbd26 100644 --- a/tests/corpus/7005-dyn-constructor-instanceof/main.ts +++ b/tests/corpus/7005-dyn-constructor-instanceof/main.ts @@ -3,9 +3,10 @@ // an ES5 constructor's prototype, at module load and in calls, and throws a // TypeError for an arrow function, which has no prototype, when the value // is an object. -import { arrowCheck, describe, rejects, tags } from "tag-types"; +import { arrowCheck, arrowObjects, describe, rejects, tags } from "tag-types"; console.log(tags()); console.log(describe("{}"), describe("[1]"), describe("null"), describe('"s"')); console.log(rejects('{"tag":"!x"}')); console.log(arrowCheck("{}"), arrowCheck("1")); +console.log(arrowObjects()); diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts index adeea728e..5b451adfa 100644 --- a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.d.ts @@ -2,3 +2,4 @@ export declare function describe(json: string): string; export declare function tags(): string; export declare function rejects(json: string): string; export declare function arrowCheck(json: string): string; +export declare function arrowObjects(): string; diff --git a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js index 20eee362f..81165bd58 100644 --- a/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js +++ b/tests/corpus/7005-dyn-constructor-instanceof/node_modules/tag-types/index.js @@ -59,3 +59,13 @@ module.exports.arrowCheck = function (json) { return error.name + ": " + error.message; } }; +// Every object reaches the TypeError, typed arrays, promises and Maps too. +module.exports.arrowObjects = function () { + return [new Uint8Array(2), Promise.resolve(1), new Map(), [1]].map(function (value) { + try { + return String(value instanceof make); + } catch (error) { + return error.name; + } + }).join(","); +}; diff --git a/tests/coverage-fixtures/blocked-signature-body.ts b/tests/coverage-fixtures/blocked-signature-body.ts index 7f8db4eb8..2c37484e0 100644 --- a/tests/coverage-fixtures/blocked-signature-body.ts +++ b/tests/coverage-fixtures/blocked-signature-body.ts @@ -13,3 +13,7 @@ function unused(target: WeakRef): number { return extra === undefined ? 0 : 1; } console.log(measure(new WeakRef({}), "abc")); +// A declared `this` parameter gives the body its receiver. +function described(this: { label: string }, target: WeakRef): string { + return this.label + String(target); +} diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 40b55b078..3ed562aea 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -194,17 +194,21 @@ test("the body of a signature-blocked function reports its diagnostics", () => { "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", ]); - // Nothing calls unused: its signature and body report as unreached. + // Nothing calls unused or described: their signatures and bodies report + // as unreached. described's declared `this` is its receiver, so + // `this.label` reports nothing. expect(sites(coverage.unreached?.diagnostics ?? [])).toEqual([ "SC2004 uses of 'extra' inherit the blocker on its declaration", + "SC2004 uses of 'target' inherit the blocker on its declaration", + "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", "SC2020 'WeakRef' is part of the standard library types but has no scriptc lowering yet", "SC2020 'new WeakRef' is part of the standard library types but has no scriptc lowering yet", ]); // The bodies' statements still count once each, as not static. const un = coverage.unreached?.stats; - expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(2); - expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(6); - expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(6); + expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(3); + expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(7); + expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(7); }); test("deferred fences in a module's top-level code report as running at startup", () => { From 0078dc2c46c93e6443fae1e3296456129ba190e0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:10:59 -0500 Subject: [PATCH 044/191] 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 de6ef6590f6e9678797d3eacd8453ff37a780236 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:12:31 -0500 Subject: [PATCH 045/191] Read optional elements at a proven integer index without a round trip An element read that may be undefined, lowered inline from `%arr.idxOr` (`a[i]` as `T | undefined`, and the read in `a[x] = a[y]!`), took an integer index only from a loop's integer slot. Any other index, such as a double counter with a proven integer range or `k | 0`, was converted and tested for an exact integer on every read. The read now takes the same exact integer as a borrowed read does (`exactInteger`), on 64-bit targets. The loop-induction treatment above the hole-free prefix still applies only to a loop's integer slot. --- .../backend/llvm/local-array-reads.test.ts | 14 ++++ .../src/backend/llvm/local-array-reads.ts | 13 +++- .../7143-optional-read-integer-index.ts.json | 6 ++ .../7143-optional-read-integer-index.ts | 70 +++++++++++++++++++ .../7143-optional-read-integer-index.ts.json | 1 + 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json create mode 100644 tests/corpus/7143-optional-read-integer-index.ts create mode 100644 tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts index b09029865..7af495d42 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -940,3 +940,17 @@ test("a dense read tests the hole-free prefix before the capacity", () => { expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled); } }); + +test("an optional read at a proven integer index skips the double round trip", () => { + const module = fixture(); + const call = module.functions[2]!.body[0]!; + if (call.kind !== "varDecl" || call.init?.kind !== "call") throw new Error("missing call"); + call.init.args[1] = { kind: "bin", op: "|", left: ref("i", F64), right: num(0), type: F64, loc }; + expect(candidates(module).get("value")?.borrow).toBe(true); + expect(validateModule(module)).toEqual([]); + // The int32 index is the dense offset on 64-bit targets; a 32-bit target + // converts the double, as a borrowed read does. + expect(workBody(module, 64)).not.toContain("local.array.exact"); + expect(workBody(module, 64)).toContain("local.array.value"); + expect(workBody(module, 32)).toContain("local.array.exact"); +}); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index cdd00c4e7..7a593e865 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -644,8 +644,15 @@ 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); + // A proven integer index (a counter with a known range, say) needs no + // double round trip, as in a borrowed read. + const exact = + inline && loopIndex === null && host.sizeType === "i64" + ? exactInteger(host, index, read.index) + : null; + const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : 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 +672,7 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, store, no, slow, @@ -726,7 +733,7 @@ export function emitLocalArrayRead( array, index, integerIndex, - integerIndex !== null, + loopIndex !== null, storeValue, no, slow, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..ae0d1f610 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7143-optional-read-integer-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7143-optional-read-integer-index.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7143-optional-read-integer-index.ts b/tests/corpus/7143-optional-read-integer-index.ts new file mode 100644 index 000000000..43fd59a6e --- /dev/null +++ b/tests/corpus/7143-optional-read-integer-index.ts @@ -0,0 +1,70 @@ +// Element reads that may be undefined, at indices with a proven integer +// range: loop counters with an unknown limit, `i | 0`, `x >>> 0`, negative +// counters, -0, indices past the length and the capacity, and copies +// `a[x] = b[y]!` that meet holes. +function copy(src: number[], n: number): number[] { + const out: number[] = new Array(n); + for (let i = 0; i < n; i++) out[i] = src[i]!; + return out; +} +const src: number[] = [1, 2, 3, 4]; +const copied = copy(src, 6); +console.log(copied.length, copied.join(","), 4 in copied, copied[5] === undefined); + +const holes: number[] = new Array(5); +holes[0] = 10; +holes[3] = 13; +const fromHoles = copy(holes, 5); +console.log(fromHoles.join(","), 1 in fromHoles, 3 in fromHoles); + +function show(a: (string | undefined)[], from: number, to: number): string { + const out: string[] = []; + for (let i = from; i < to; i++) { + const v = a[i]; + out.push(v === undefined ? "_" : v); + } + return out.join(","); +} +const words: (string | undefined)[] = ["a", undefined, "c"]; +words[5] = "f"; +console.log(show(words, -3, 8)); +console.log(show(words, 0, 0), show(words, 2, 1)); + +function masked(a: number[], keys: number[]): string { + const out: string[] = []; + for (const k of keys) { + const v = a[k | 0]; + const w = a[k >>> 0]; + out.push(`${v === undefined ? "-" : v}/${w === undefined ? "-" : w}`); + } + return out.join(" "); +} +const values: number[] = [5, 6, 7]; +console.log(masked(values, [0, 1.9, 2, 3, -1, -0, 4294967297, 2 ** 31])); + +const negativeZero = -0; +console.log(values[negativeZero], values[0 * -1], Object.is(negativeZero, -0)); + +// A counter that starts below zero and one that passes the capacity. +let total = 0; +for (let i = -2; i < values.length + 3; i++) total += values[i] ?? 100; +console.log(total); +const big: number[] = []; +for (let k = 0; k < 20; k++) big.push(k); +big.length = 10; +let seen = 0; +for (let i = 0; i < 40; i++) if (big[i] !== undefined) seen++; +console.log(seen, big.length); + +// Rotations through the read, with a hole that becomes undefined. +function rotate(p: number[], r: number): void { + const first = p[0]!; + for (let i = 0; i < r; i++) p[i] = p[i + 1]!; + p[r] = first; +} +const ring: number[] = new Array(4); +ring[0] = 0; +ring[1] = 1; +ring[3] = 3; +rotate(ring, 3); +console.log(ring.join(","), 1 in ring, ring[1] === undefined); diff --git a/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json new file mode 100644 index 000000000..16148d8ce --- /dev/null +++ b/tests/harness/corpus-costs/7143-optional-read-integer-index.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 5400 } From 3a3610d12ebfaedce85f3e877338500da9e7f8dc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:15:10 -0500 Subject: [PATCH 046/191] Look up spawn and execFileSync commands on the replacement environment's PATH spawn and execFileSync with an `env` option searched the parent's PATH for a command without a '/'. They now resolve it the way spawnSync does, as Node (libuv) does: on the replacement environment's PATH, or the default path when it has none, with relative entries read from the cwd option's directory. --- .../corpus/7036-child-env-path-lookup.ts.json | 6 ++ packages/runtime/src/scr_child.c | 62 +++++++++++++------ tests/corpus/7036-child-env-path-lookup.ts | 40 ++++++++++++ .../7036-child-env-path-lookup.ts.json | 1 + 4 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json create mode 100644 tests/corpus/7036-child-env-path-lookup.ts create mode 100644 tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..171ab060a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7036-child-env-path-lookup.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7036-child-env-path-lookup.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index df60fcb26..c1d98b35b 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -3116,9 +3116,12 @@ static void scr_exec_envp_free(char **envp); * environment, found as libuv finds it: a name with a '/' is used as * given; otherwise each entry of the replacement's PATH (an empty entry * is the current directory), or of the default path when the replacement - * has no PATH, is tried in order. Returns a malloc'd path, or NULL with - * *err set: EACCES when a candidate exists but cannot run, else ENOENT. */ -static char *scr_spawn_path_lookup(const char *file, char *const *envp, int *err) { + * has no PATH, is tried in order. Relative candidates are checked from + * `cwd`, the directory the child starts in (NULL: this process's). Returns + * a malloc'd path, or NULL with *err set: EACCES when a candidate exists + * but cannot run, else ENOENT. */ +static char *scr_spawn_path_lookup(const char *file, char *const *envp, const char *cwd, + int *err) { if (strchr(file, '/')) { char *copy = strdup(file); if (!copy) scr_child_oom(); @@ -3132,6 +3135,7 @@ static char *scr_spawn_path_lookup(const char *file, char *const *envp, int *err } } size_t file_len = strlen(file); + size_t cwd_len = cwd ? strlen(cwd) : 0; bool denied = false; for (const char *dir = path;;) { const char *end = strchr(dir, ':'); @@ -3143,13 +3147,24 @@ static char *scr_spawn_path_lookup(const char *file, char *const *envp, int *err memcpy(candidate, prefix, prefix_len); candidate[prefix_len] = '/'; memcpy(candidate + prefix_len + 1, file, file_len + 1); + char *checked = candidate; + if (cwd_len > 0 && candidate[0] != '/') { + checked = malloc(cwd_len + 1 + strlen(candidate) + 1); + if (!checked) scr_child_oom(); + memcpy(checked, cwd, cwd_len); + checked[cwd_len] = '/'; + strcpy(checked + cwd_len + 1, candidate); + } struct stat st; - if (stat(candidate, &st) == 0) { - if (S_ISREG(st.st_mode) && access(candidate, X_OK) == 0) return candidate; - denied = true; + bool runs = false; + if (stat(checked, &st) == 0) { + runs = S_ISREG(st.st_mode) && access(checked, X_OK) == 0; + if (!runs) denied = true; } else if (errno == EACCES) { denied = true; } + if (checked != candidate) free(checked); + if (runs) return candidate; free(candidate); if (!end) break; dir = end + 1; @@ -3158,6 +3173,22 @@ static char *scr_spawn_path_lookup(const char *file, char *const *envp, int *err return NULL; } +/* posix_spawnp for the child_process spawns: with `envp` (a replacement + * environment) the command resolves on that environment's PATH + * (scr_spawn_path_lookup) as Node's does; without it, on this process's + * PATH over `environ`. Returns posix_spawn's error number. */ +static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, char *const *argv, + char *const *envp, const char *cwd) { + if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ); + int err = 0; + char *resolved = scr_spawn_path_lookup(file, envp, cwd, &err); + if (!resolved) return err; + err = posix_spawn(pid, resolved, fa, attr, argv, envp); + free(resolved); + return err; +} + /* env_pairs: NULL inherits the parent's environment; else [k,v,...] * replaces it, as Node's `env` option does, and the command is looked up * on the replacement's PATH (scr_spawn_path_lookup). */ @@ -3200,16 +3231,7 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou char **argv = scr_child_argv(cmd, args); char **envp = env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; - int spawn_err; - if (envp != NULL) { - char *resolved = scr_spawn_path_lookup(cmd->data, envp, &spawn_err); - if (resolved) { - spawn_err = posix_spawn(&pid, resolved, &fa, NULL, argv, envp); - free(resolved); - } - } else { - spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ); - } + int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, NULL); posix_spawn_file_actions_destroy(&fa); free(argv); scr_exec_envp_free(envp); @@ -3556,8 +3578,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { char **argv = scr_child_argv(cmd, args); char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL; pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, - envp != NULL ? envp : environ); + int spawn_err = scr_spawn_command(&pid, cmd->data, &fa, NULL, argv, envp, + o->cwd != NULL ? o->cwd->data : NULL); posix_spawn_file_actions_destroy(&fa); free(argv); scr_exec_envp_free(envp); @@ -5115,8 +5137,8 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; pid_t pid = -1; const char *file = shell ? "/bin/sh" : cmd->data; - int spawn_err = posix_spawnp(&pid, file, &fa, &at, argv, - envp != NULL ? envp : environ); + int spawn_err = scr_spawn_command(&pid, file, &fa, &at, argv, envp, + cwd != NULL && cwd->len > 0 ? cwd->data : NULL); posix_spawnattr_destroy(&at); posix_spawn_file_actions_destroy(&fa); if (!shell) free(argv); diff --git a/tests/corpus/7036-child-env-path-lookup.ts b/tests/corpus/7036-child-env-path-lookup.ts new file mode 100644 index 000000000..cac0670e9 --- /dev/null +++ b/tests/corpus/7036-child-env-path-lookup.ts @@ -0,0 +1,40 @@ +// spawn and execFileSync with an env option find the command on the +// replacement environment's PATH, or on the default path when it has +// none, as Node does; a relative PATH entry is read from the cwd option. +import { execFileSync, spawn } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-child-path-")); +mkdirSync(join(dir, "tools")); +writeFileSync(join(dir, "tools", "child-tool"), "#!/bin/sh\necho child-tool $1\n"); +chmodSync(join(dir, "tools", "child-tool"), 0o755); + +const show = (e: unknown): string => { + const err = e as NodeJS.ErrnoException; + return `${err.code} ${err.message.replace(dir, "")}`; +}; + +console.log(execFileSync("child-tool", ["sync"], { env: { PATH: join(dir, "tools") }, encoding: "utf8" }).trim()); +console.log(execFileSync("sh", ["-c", "echo default"], { env: { A: "1" }, encoding: "utf8" }).trim()); +try { + execFileSync("sh", ["-c", "echo never"], { env: { PATH: "/nonexistent" }, encoding: "utf8" }); +} catch (e) { + console.log(show(e)); +} +console.log( + execFileSync("child-tool", ["relative"], { env: { PATH: "tools" }, cwd: dir, encoding: "utf8" }).trim(), +); + +const child = spawn("child-tool", ["async"], { env: { PATH: join(dir, "tools") }, stdio: ["ignore", "pipe", "ignore"] }); +let out = ""; +child.stdout!.on("data", (chunk: Buffer) => (out += chunk.toString())); +child.on("close", (code) => { + console.log(out.trim(), code); + const missing = spawn("sh", ["-c", "true"], { env: { PATH: "/nonexistent" }, stdio: "ignore" }); + missing.on("error", (e) => { + console.log("error", show(e)); + rmSync(dir, { recursive: true }); + }); +}); diff --git a/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json new file mode 100644 index 000000000..f9bfce7d9 --- /dev/null +++ b/tests/harness/corpus-costs/7036-child-env-path-lookup.ts.json @@ -0,0 +1 @@ +{ "plain": 2570, "san": 4110 } From 6e79566c255877f002203ac0eeeac0cd1928e9a4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:18:57 -0500 Subject: [PATCH 047/191] 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 048/191] 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 ccfca67aecc819a66e38baafe01e213f99f1337f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:30:56 -0500 Subject: [PATCH 049/191] Fix chalk builder field calls and hot-shots send failures and tcp completion --- packages/compiler/shims/hot-shots/client.ts | 23 ++++++++++--- .../src/frontend/lowering/lower-calls.ts | 4 +++ .../npm/cases/chalk5-builder/main.ts.json | 1 + .../fixtures/npm/cases/chalk5/main.ts.json | 1 + .../npm/cases/hot-shots-buffered/main.ts.json | 2 ++ .../cases/hot-shots-send-error/main.ts.json | 8 +++++ .../npm/cases/hot-shots-tcp/main.ts.json | 8 +++++ .../npm/cases/hot-shots-udp/main.ts.json | 2 ++ packages/runtime/src/scr_dgram.h | 7 ++++ .../fixtures/npm/cases/chalk5-builder/main.ts | 5 ++- .../npm/cases/hot-shots-send-error/main.ts | 27 +++++++++++++++ .../cases/hot-shots-send-error/package.json | 1 + .../fixtures/npm/cases/hot-shots-tcp/main.ts | 33 +++++++++++++++++++ .../npm/cases/hot-shots-tcp/package.json | 1 + 14 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json create mode 100644 tests/fixtures/npm/cases/hot-shots-send-error/main.ts create mode 100644 tests/fixtures/npm/cases/hot-shots-send-error/package.json create mode 100644 tests/fixtures/npm/cases/hot-shots-tcp/main.ts create mode 100644 tests/fixtures/npm/cases/hot-shots-tcp/package.json diff --git a/packages/compiler/shims/hot-shots/client.ts b/packages/compiler/shims/hot-shots/client.ts index b407132f4..354744e7b 100644 --- a/packages/compiler/shims/hot-shots/client.ts +++ b/packages/compiler/shims/hot-shots/client.ts @@ -179,6 +179,8 @@ interface Transport { close(): void; onError(listener: (err: Error) => void): void; offError(listener: (err: Error) => void): void; + /** Emits 'error' on the socket, as hot-shots' `socket.emit('error')`. */ + emitError(err: Error): void; onClose(listener: () => void): void; } @@ -261,6 +263,9 @@ class UdpTransport implements Transport { offError(listener: (err: Error) => void): void { this.errors.remove(listener); } + emitError(err: Error): void { + this.errors.emit(err); + } onClose(listener: () => void): void { this.socket.on("close", listener); } @@ -281,10 +286,10 @@ class TcpTransport implements Transport { this.socket.on("error", (err) => errors.emit(err)); } send(buf: Buffer, callback: SendCallback): void { - // The typed socket's write takes the data alone; the completion runs - // on the next tick. - this.socket.write(addEol(buf)); - process.nextTick(() => callback(null)); + // hot-shots writes the line with the 'ascii' encoding, which encodes + // strings as 'latin1' does. The callback runs once the data is written, + // so a message held back by a full socket buffer stays in flight. + this.socket.write(Buffer.from(addEol(buf), "latin1"), (err) => callback(err ?? null)); } close(): void { this.socket.destroy(); @@ -295,6 +300,9 @@ class TcpTransport implements Transport { offError(listener: (err: Error) => void): void { this.errors.remove(listener); } + emitError(err: Error): void { + this.errors.emit(err); + } onClose(listener: () => void): void { this.socket.on("close", () => listener()); } @@ -376,6 +384,9 @@ class UdsTransport implements Transport { offError(listener: (err: Error) => void): void { this.errors.remove(listener); } + emitError(err: Error): void { + this.errors.emit(err); + } onClose(listener: () => void): void { this.closeListeners.push(listener); } @@ -999,6 +1010,10 @@ export class StatsD { handler(errFormatted); } else { console.error(String(errFormatted)); + // hot-shots also emits the error on the current socket, where the + // uds reconnection handler listens; with no listener it throws. + const current = this.transport; + if (current !== null) current.emitError(errFormatted); } } }; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b650ca567..301670c3a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -9250,6 +9250,10 @@ export function lowerRecordFieldCall( if (callee.type.kind === "object" && !init) { const direct = callableInstanceStringCall(lowerer, call, callee, loc); if (direct) return direct; + // Other argument counts call the %call closure through its value, as + // a call on the builder itself does (`theme.error("a", "b")`). + const closure = lowerer.hybridCallUnwrap(callee); + if (closure !== callee) return lowerCallThroughValue(lowerer, call, closure, loc); } if (callee.type.kind === "record" || callee.type.kind === "object") callee = lowerer.hybridCallUnwrap(callee); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json index 545c4d701..8f7268daf 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5-builder/main.ts.json @@ -1,5 +1,6 @@ { "order": [ + "/packages/compiler/shims/chalk/v5.ts", "/tests/fixtures/npm/cases/chalk5-builder/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json index 065c96347..8365d4db9 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json @@ -1,5 +1,6 @@ { "order": [ + "/packages/compiler/shims/chalk/v5.ts", "/tests/fixtures/npm/cases/chalk5/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json index e60660c09..a48e1af78 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-buffered/main.ts.json @@ -1,5 +1,7 @@ { "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", "/tests/fixtures/npm/cases/hot-shots-buffered/main.ts" ], "diags": [] diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json new file mode 100644 index 000000000..716cfe02f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-send-error/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-send-error/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json new file mode 100644 index 000000000..f131361e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-tcp/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", + "/tests/fixtures/npm/cases/hot-shots-tcp/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json index 54baff890..a3014fb9f 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hot-shots-udp/main.ts.json @@ -1,5 +1,7 @@ { "order": [ + "/packages/compiler/shims/hot-shots/client.ts", + "/packages/compiler/shims/hot-shots/v8.ts", "/tests/fixtures/npm/cases/hot-shots-udp/main.ts" ], "diags": [] diff --git a/packages/runtime/src/scr_dgram.h b/packages/runtime/src/scr_dgram.h index 8396f0693..08db077df 100644 --- a/packages/runtime/src/scr_dgram.h +++ b/packages/runtime/src/scr_dgram.h @@ -55,6 +55,13 @@ double scr_dgram_addr_port(ScrDgramSocket *s); void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb /*moves, nullable*/); /* throws on closed */ void scr_dgram_unref(ScrDgramSocket *s); void scr_dgram_ref(ScrDgramSocket *s); +/* Unix datagram sockets for the hot-shots package shim (the unix-dgram + * addon's surface): each answers its result (fd, 0) or a negative errno; + * send answers 1 when the socket would block. */ +double scr_dgram_unix_socket(void); +double scr_dgram_unix_connect(double fd, ScrStr *path /*borrowed*/); +double scr_dgram_unix_send(double fd, ScrBytes *data /*borrowed*/); +double scr_dgram_unix_close(double fd); /* Socket options: Node's checks and errors (ErrnoException " * ", ERR_SOCKET_BAD_BUFFER_SIZE, ERR_SOCKET_BUFFER_SIZE, the * TypeError after close()). The TTL setters and setMulticastLoopback diff --git a/tests/fixtures/npm/cases/chalk5-builder/main.ts b/tests/fixtures/npm/cases/chalk5-builder/main.ts index 8f2623717..2a4fff61b 100644 --- a/tests/fixtures/npm/cases/chalk5-builder/main.ts +++ b/tests/fixtures/npm/cases/chalk5-builder/main.ts @@ -1,6 +1,7 @@ // chalk 5 builders used as values (the package compiles from scriptc's // built-in shim): builders in function slots and arrays, styles picked at -// run time, visible at level 0, chalkStderr, and the root's own call. +// run time, builders in record fields called with any number of arguments, +// visible at level 0, chalkStderr, and the root's own call. import chalk, { Chalk, chalkStderr, type ChalkInstance, type ForegroundColorName } from "chalk"; const show = (label: string, value: string): void => { @@ -36,6 +37,8 @@ const off = new Chalk({ level: 0 }); show("visible off", off.visible("v") + "|" + off.red.visible("v") + "|" + off.red("plain")); show("root call", c("a", 1, true) + "|" + c() + "|" + c(undefined) + "|" + c(null, "n")); show("builder args", c.red(undefined) + "|" + c.red(null) + "|" + c.red(1, 2) + "|" + c.red()); +const theme = { error: c.red, note: c.bold.blue }; +show("field args", theme.error("a", "b") + "|" + theme.note("x", 1, true) + "|" + theme.error("one") + "|" + theme.error()); show("multiline", c.cyan("one\r\ntwo\nthree")); show("stderr level", String(chalkStderr.level >= 0)); show("rgb levels", [1, 2, 3].map((level) => new Chalk({ level: level as 1 | 2 | 3 }).rgb(255, 136, 0)("o")).join("|")); diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/main.ts b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts new file mode 100644 index 000000000..e829c5e33 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/main.ts @@ -0,0 +1,27 @@ +// The hot-shots StatsD client (compiled from scriptc's built-in shim) when a +// send fails: with a callback or an errorHandler the error goes there; with +// neither, the client logs it and emits it on the socket, where an 'error' +// event nobody listens for ends the process with status 1. +import { StatsD } from "hot-shots"; + +// A datagram larger than udp allows fails with EMSGSIZE before it leaves +// the host; port 9 keeps the message the same on every run. +const big = "x".repeat(70000); + +const withCallback = new StatsD({ host: "127.0.0.1", port: 9 }); +withCallback.increment(big, 1, 1, [], (err: Error | null | undefined) => { + console.log("callback:", err ? err.message : "no error"); + withCallback.close(); + + const withHandler = new StatsD({ + host: "127.0.0.1", + port: 9, + errorHandler: (error: Error) => { + console.log("errorHandler:", error.message); + withHandler.close(); + console.log("without a callback or errorHandler"); + new StatsD({ host: "127.0.0.1", port: 9 }).increment(big); + }, + }); + withHandler.increment(big); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-send-error/package.json b/tests/fixtures/npm/cases/hot-shots-send-error/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-send-error/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/main.ts b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts new file mode 100644 index 000000000..0812c90f1 --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/main.ts @@ -0,0 +1,33 @@ +// The hot-shots StatsD client over tcp (compiled from scriptc's built-in +// shim): a send completes once the socket has written its line, so close() +// waits for lines that a full socket buffer holds back, and the receiver +// gets every line before the connection ends. +import * as net from "node:net"; +import { StatsD } from "hot-shots"; + +const LINES = 20000; +const name = "m".repeat(1000); +let bytes = 0; +let ended: () => void = () => {}; +const serverDone = new Promise((resolve) => { + ended = resolve; +}); +const server = net.createServer((socket) => { + socket.pause(); + setTimeout(() => socket.resume(), 150); + socket.on("data", (chunk: Buffer) => { + bytes += chunk.length; + }); + socket.on("end", () => ended()); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const client = new StatsD({ host: "127.0.0.1", port, protocol: "tcp" }); + for (let i = 0; i < LINES; i++) client.increment(name); + const closed = new Promise((resolve) => client.close(() => resolve())); + void Promise.all([closed, serverDone]).then(() => { + console.log("closed; received", bytes, "of", LINES * (name.length + 5), "bytes"); + server.close(() => console.log("server closed")); + }); +}); diff --git a/tests/fixtures/npm/cases/hot-shots-tcp/package.json b/tests/fixtures/npm/cases/hot-shots-tcp/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/fixtures/npm/cases/hot-shots-tcp/package.json @@ -0,0 +1 @@ +{ "type": "module" } From a8a545a81f9fd6aa5132977d8e26715d4c7df462 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:36:22 -0500 Subject: [PATCH 050/191] 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 051/191] 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 4d7de0305411bae6db0d7306d739baadbe3f906e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:46:16 -0500 Subject: [PATCH 052/191] Store arrays of ProgramLifetime instances in 4-byte slots on Linux --- docs/content/docs/contracts.mdx | 2 +- .../compiler/src/backend/llvm/array-search.ts | 37 ++-- .../compiler/src/backend/llvm/classes.test.ts | 117 ++++++++++- packages/compiler/src/backend/llvm/classes.ts | 22 +- .../src/backend/llvm/dense-array-access.ts | 88 ++++++-- .../compiler/src/backend/llvm/emit-module.ts | 4 +- .../src/backend/llvm/emit-statements.ts | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 63 ++++-- .../src/backend/llvm/expr-containers.ts | 19 +- .../src/backend/llvm/expr-primitives.ts | 10 +- .../src/backend/llvm/lib-filesystem.ts | 8 +- .../src/backend/llvm/local-array-reads.ts | 36 +++- packages/compiler/src/backend/llvm/shapes.ts | 20 +- ...8-contract-program-lifetime-arrays.ts.json | 6 + packages/runtime/src/scr_array.c | 196 ++++++++++++------ packages/runtime/src/scr_array.h | 44 +++- packages/runtime/src/scr_copying.c | 2 +- packages/runtime/src/scr_lib.c | 2 +- packages/runtime/src/scr_publish.c | 20 +- .../4638-contract-program-lifetime-arrays.ts | 117 +++++++++++ ...8-contract-program-lifetime-arrays.ts.json | 1 + 21 files changed, 640 insertions(+), 176 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json create mode 100644 tests/corpus/4638-contract-program-lifetime-arrays.ts create mode 100644 tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index 5af1cad2f..d3bd3c7f4 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -349,7 +349,7 @@ class Identifier extends SyntaxNode { } ``` -On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8. In the example, `parent` takes 4 bytes. +On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8, as does each element of an array of such a type. In the example, `parent` takes 4 bytes. scriptc checks that the promise cannot change what the program computes (SC7008): diff --git a/packages/compiler/src/backend/llvm/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts index d61d91e5c..41b64d418 100644 --- a/packages/compiler/src/backend/llvm/array-search.ts +++ b/packages/compiler/src/backend/llvm/array-search.ts @@ -10,6 +10,8 @@ * SameValueZero are both pointer identity (class instances, records, * arrays, maps, sets, typed arrays): 64-bit compares, four slots per * branch. + * - sc_arr_index_of_cref, the same over the 4-byte slots of compact class + * references (SCR_ELEM_CREF), against the needle's 32-bit reference. * - sc_arr_index_of_str, for strings: pointer identity, then the length, * then the bytes. Strings of up to 16 bytes compare as two words; a short * string's word is the eight bytes ending at its last byte, which start @@ -26,10 +28,12 @@ import type { IrType } from "../../ir/ir.js"; import { SCR_ARR_FIELD } from "./common.js"; import { FN_ATTRS } from "./shapes.js"; -export type ArraySearchKind = "ptr" | "str"; +export type ArraySearchKind = "ptr" | "cref" | "str"; -/** The inline search for arrays of `elem`, or null for the runtime search. */ -export function arraySearchKind(elem: IrType): ArraySearchKind | null { +/** The inline search for arrays of `elem`, or null for the runtime search. + * `compact`: the arrays hold compact class references (SCR_ELEM_CREF). */ +export function arraySearchKind(elem: IrType, compact: boolean): ArraySearchKind | null { + if (compact) return elem.kind === "object" ? "cref" : null; switch (elem.kind) { case "object": case "record": @@ -46,7 +50,7 @@ export function arraySearchKind(elem: IrType): ArraySearchKind | null { } export function arraySearchHelperName(kind: ArraySearchKind): string { - return kind === "ptr" ? "sc_arr_index_of_ptr" : "sc_arr_index_of_str"; + return `sc_arr_index_of_${kind}`; } /** The definitions of the helpers in `kinds`; they declare their slow paths @@ -76,9 +80,18 @@ export function emitArraySearchHelpers( ` %dp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 ${SCR_ARR_FIELD.data}`, ` %data = load ptr, ptr %dp`, ); - if (kind === "ptr") { + if (kind === "ptr" || kind === "cref") { + // A compact reference is the address over 8 (classes.ts + // compactRefLoadLines). + const slot = kind === "ptr" ? "i64" : "i32"; defs.push( - ` %needle = ptrtoint ptr %v to i64`, + ...(kind === "ptr" + ? [` %needle = ptrtoint ptr %v to i64`] + : [ + ` %vaddr = ptrtoint ptr %v to i64`, + ` %vref = lshr i64 %vaddr, 3`, + ` %needle = trunc i64 %vref to i32`, + ]), // Four slots per step with one branch, then one at a time. ` br label %quad`, `quad:`, @@ -89,9 +102,9 @@ export function emitArraySearchHelpers( `quadbody:`, ...[0, 1, 2, 3].flatMap((k) => [ ` %qi${k} = add ${sz} %q, ${k}`, - ` %qp${k} = getelementptr inbounds i64, ptr %data, ${sz} %qi${k}`, - ` %qs${k} = load i64, ptr %qp${k}`, - ` %qh${k} = icmp eq i64 %qs${k}, %needle`, + ` %qp${k} = getelementptr inbounds ${slot}, ptr %data, ${sz} %qi${k}`, + ` %qs${k} = load ${slot}, ptr %qp${k}`, + ` %qh${k} = icmp eq ${slot} %qs${k}, %needle`, ]), ` %qh01 = or i1 %qh0, %qh1`, ` %qh23 = or i1 %qh2, %qh3`, @@ -107,9 +120,9 @@ export function emitArraySearchHelpers( ` %done = icmp eq ${sz} %i, %len`, ` br i1 %done, label %miss, label %body`, `body:`, - ` %sp = getelementptr inbounds i64, ptr %data, ${sz} %i`, - ` %s = load i64, ptr %sp`, - ` %hit = icmp eq i64 %s, %needle`, + ` %sp = getelementptr inbounds ${slot}, ptr %data, ${sz} %i`, + ` %s = load ${slot}, ptr %sp`, + ` %hit = icmp eq ${slot} %s, %needle`, ` br i1 %hit, label %found, label %cont`, ); } else { diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 2d95e942c..f921900a3 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,5 +1,14 @@ import { expect, test } from "vitest"; -import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; +import { + DYN, + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrModule, + type IrType, +} from "../../ir/ir.js"; import { assignCompactLayouts, buildClassGraph, @@ -222,12 +231,13 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => { contracts: { programLifetime: ["Node"] }, }; const llvm = emitLlvmModule(mod, { pointerBits: 64 }); - // Fields go widest first, each at the first offset that fits it. Node's - // weight (an int32 slot: nothing stores a fraction) takes the word the - // header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its - // bool next to Node's (32 bytes, not 48). - expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`); - expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`); + // Scalars narrower than 8 bytes go first, then the rest, widest first, + // each at the first offset that fits it. Node's weight (an int32 slot: + // nothing stores a fraction) takes the word the header shares and its + // flag the next one (24 bytes, not 32); Leaf keeps Node's offsets and puts + // its bool next to Node's (32 bytes, not 48). + expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, i8, ptr }`); + expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, i8, i8, ptr, ptr }`); // Without the contract, the layout is the declared one. const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 }); expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`); @@ -332,6 +342,99 @@ test("references between ProgramLifetime instances are 32-bit indexes into their expect(classFieldIndex(leafMeta, "label").compressed).toBe(false); }); +test("arrays of ProgramLifetime instances keep 32-bit references in 4-byte slots", () => { + const node = { kind: "object" as const, className: "Node" }; + const nodes = { kind: "array" as const, elem: node }; + const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); + const fn = (name: string, body: IrExpr, returnType: IrType): IrFunction => ({ + name, + params: [ + { name: "a", localId: "a", type: nodes }, + { name: "v", localId: "v", type: node }, + ], + returnType, + locals: [ + { id: "a", name: "a", type: nodes, mutable: false }, + { id: "v", name: "v", type: node, mutable: false }, + ], + body: [{ kind: "return", value: body, loc }], + loc, + }); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn( + "first", + { + kind: "arrayGet", + arr: ref("a", nodes), + index: { kind: "numLit", value: 0, type: F64, loc }, + type: node, + loc, + }, + node, + ), + fn( + "add", + { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + fn("make", { kind: "arrayLit", elems: [ref("v", node)], type: nodes, loc }, nodes), + fn( + "find", + { + kind: "arrIntrinsic", + method: "indexOf", + receiver: ref("a", nodes), + args: [ref("v", node)], + type: F64, + loc, + }, + F64, + ), + ], + classes: [{ name: "Node", fields: [{ name: "kids", type: nodes }], methods: [], loc }], + contracts: { programLifetime: ["Node"] }, + }; + const body = (llvm: string, name: string): string => + llvm.split(/^define /m).find((f) => f.includes(`; ${name}\n`)) ?? ""; + const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true }); + // The element type's ops say so (SCR_ARR_OPS_COMPACT), so the runtime makes + // the arrays SCR_ELEM_CREF. + expect(llvm).toMatch( + /^@sc_arrops\.\S+\.c = internal global \{ ptr, ptr, ptr, i32, i32 \} \{ [^}]*, i32 0, i32 1 \}$/m, + ); + // A read loads the 32-bit reference and shifts it; an append stores one. + const first = body(llvm, "first"); + expect(first).toMatch(/getelementptr inbounds i32, ptr %\S+, i64 \S+\n\s+%\S+ = load i32/); + // The states follow 4-byte slots. + expect(first).toContain("getelementptr inbounds [5 x i8]"); + expect(first).toMatch(/= inttoptr i64 %\S+ to ptr/); + expect(body(llvm, "add")).toMatch( + /= lshr i64 %\S+, 3\n\s+%\S+ = trunc i64 %\S+ to i32\n\s+store i32/, + ); + // indexOf scans the 4-byte slots for the needle's reference. + expect(llvm).toMatch(/call double @sc_arr_index_of_cref\(ptr %\S+, ptr %\S+\)/); + expect(/^define internal double @sc_arr_index_of_cref[^]*?^}/m.exec(llvm)?.[0]).toContain( + "load i32, ptr %qp0", + ); + // Elsewhere the arrays keep 8-byte slots. + const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false }); + expect(elsewhere).not.toMatch(/sc_arrops\.\S+\.c /); + expect(body(elsewhere, "first")).not.toContain("inttoptr"); + expect(elsewhere).not.toContain("sc_arr_index_of_cref"); +}); + test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => { const BOOL = { kind: "bool" as const }; const mod: IrModule = { diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bd..8f179828d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -703,11 +703,13 @@ export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32S /** Gives the classes `compact` admits (whole hierarchies: the test is the * root's) the 32-bit header and packed fields. A class keeps its base's - * field offsets, and places each of its own fields, widest first, at the - * first offset after the header that fits it: in a hole another field's - * alignment left, in the base's tail padding, or at the end. So 32-bit and - * smaller fields share 8-byte words. The struct lists the header and the - * fields by offset, which LLVM's layout of the type reproduces (checked). */ + * field offsets, and places each of its own fields at the first offset + * after the header that fits it: in a hole another field's alignment left, + * in the base's tail padding, or at the end. Scalars narrower than 8 bytes + * (flags, ids, counts) go first, widest first, so they share the header's + * cache line; then the other fields, widest first. So 32-bit and smaller + * fields share 8-byte words. The struct lists the header and the fields by + * offset, which LLVM's layout of the type reproduces (checked). */ export function assignCompactLayouts( metaMap: ReadonlyMap, compact: (root: LlClassMeta) => boolean, @@ -751,10 +753,12 @@ export function assignCompactLayouts( ); return COMPACT_REF; }); + const smallScalar = (f: { type: string; field: number; size: number }): number => + f.size < 8 && f.type !== "ptr" && !compressed[f.field] ? 0 : 1; const own = types .map((type, field) => ({ type, field, ...fieldStorage(type) })) .slice(baseCount) - .sort((a, b) => b.size - a.size || a.field - b.field); + .sort((a, b) => smallScalar(a) - smallScalar(b) || b.size - a.size || a.field - b.field); for (const f of own) { const sorted = [...elems].sort((a, b) => a.offset - b.offset); let at = -1; @@ -860,9 +864,9 @@ export function emitClassShapes( nullable?: NullableRefFields, int32?: Int32Slots, ): { typeDefs: string[]; defs: string[] } { - // Whether compact instances live in the program-lifetime region (any - // field holds a 32-bit reference to one; emitter.ts assignCompactLayouts). - const regionRefs = [...metaMap.values()].some((m) => m.compressed.some(Boolean)); + // Whether compact instances live in the program-lifetime region, where + // fields and arrays can refer to them in 32 bits (LlEmitter.compactRegion). + const regionRefs = host.compactRegion?.() === true; const typeDefs: string[] = []; const defs: string[] = []; const emitted = (mod.classes ?? []).filter((c) => !c.runtime); diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 4488f90cb..0ea014059 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -27,6 +27,8 @@ import { storeArrCount, } from "./common.js"; 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"; const SCR_ARR_VALUE = 1; @@ -209,26 +211,40 @@ function statePointer( return statePtr; } -/** The dense slot type: boolean arrays store one byte per element, every - * other element kind an 8-byte slot. */ -function slotType(acc: ArrayAccess): "i8" | "i64" { - return acc === "bool" ? "i8" : "i64"; +/** 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). */ +type SlotKind = ArrayAccess | "cref"; + +function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind { + return acc === "ref" && host.compactArrayElem(element) ? "cref" : acc; +} + +/** The dense slot type: boolean arrays store one byte per element, compact + * references four, every other element kind an 8-byte slot. */ +function slotType(kind: SlotKind): "i8" | "i32" | "i64" { + return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64"; } /** Bytes per dense slot (scr_arr_slot_width). */ -export function slotWidth(acc: ArrayAccess): number { - return acc === "bool" ? 1 : 8; +function slotWidth(kind: SlotKind): number { + return kind === "bool" ? 1 : kind === "cref" ? 4 : 8; +} + +/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */ +export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): number { + return slotWidth(slotKind(host, elemAccess(element), element)); } function valuePointer( host: LlvmEmitterContext, guard: Pick, - acc: ArrayAccess, + kind: SlotKind, ): string { const B = host.B; const valuePtr = B.tmp(); B.line( - `${valuePtr} = getelementptr inbounds ${slotType(acc)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, + `${valuePtr} = getelementptr inbounds ${slotType(kind)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); return valuePtr; @@ -300,6 +316,7 @@ export function emitDenseArrayGet( const B = host.B; const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; 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(); @@ -322,7 +339,7 @@ export function emitDenseArrayGet( slow, true, valueLabel, - slotWidth(acc), + slotWidth(kind), proven, induction, ); @@ -333,7 +350,7 @@ export function emitDenseArrayGet( B.condBr(present, valueLabel, slow); } B.startBlock(valueLabel); - const ptr = valuePointer(host, guard, acc); + const ptr = valuePointer(host, guard, kind); let fast: string; let fastLabel = valueLabel; if (acc === "f64") { @@ -347,7 +364,13 @@ export function emitDenseArrayGet( } else { const raw = B.tmp(), nonnull = B.tmp(); - B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + if (kind === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + } B.line(`${nonnull} = icmp ne ptr ${raw}, null`); fastLabel = B.newLabel("arr.get.ref"); B.condBr(nonnull, fastLabel, slow); @@ -448,6 +471,7 @@ export function emitDenseArraySet( ): void { const B = host.B; const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; + const kind = slotKind(host, acc, element); const setter = `scr_arr_set_${acc}`; host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`); const call = (attrs: string): void => @@ -467,7 +491,7 @@ export function emitDenseArraySet( slow, false, filledLabel, - slotWidth(acc), + slotWidth(kind), proven, ); const { offset, len } = guard; @@ -478,15 +502,20 @@ export function emitDenseArraySet( const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } }; const statePtr = statePointer(host, guard); - const valuePtr = valuePointer(host, guard, acc); + const valuePtr = valuePointer(host, guard, kind); let oldState: string | null = null; let old: string | null = null; - if (acc === "ref") { + // Compact instances are never released: a replaced one needs nothing. + if (kind === "ref") { oldState = B.tmp(); old = B.tmp(); B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); @@ -537,7 +566,7 @@ export function emitDenseArraySet( storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr)); B.br(storedLabel); B.startBlock(storedLabel); - if (acc === "ref" && oldState && old) { + if (kind === "ref" && oldState && old) { const hadValue = B.tmp(); B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`); const releaseLabel = B.newLabel("arr.set.release"); @@ -549,9 +578,9 @@ export function emitDenseArraySet( // Below the bound the slot holds a present value: replace it, leaving // the state alone, and release a replaced reference after the store. B.startBlock(filledLabel); - const filledValuePtr = valuePointer(host, guard, acc); + const filledValuePtr = valuePointer(host, guard, kind); let filledOld: string | null = null; - if (acc === "ref") { + if (kind === "ref") { filledOld = B.tmp(); B.line( `${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`, @@ -578,10 +607,12 @@ export function emitDenseArrayPop( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType, empty: string, slow: string, ): string { const B = host.B; + const kind = slotKind(host, acc, element); const size = host.sizeType; const header = (field: number): string => { const ptr = B.tmp(); @@ -615,16 +646,23 @@ export function emitDenseArrayPop( states = B.tmp(); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); - scrArrStates(host, states, data, cap, slotWidth(acc)); - const valuePtr = valuePointer(host, { offset: last, data }, acc); + scrArrStates(host, states, data, cap, slotWidth(kind)); + const valuePtr = valuePointer(host, { offset: last, data }, kind); const statePtr = statePointer(host, { offset: last, states }); - const slotTy = slotType(acc); + const slotTy = slotType(kind); let raw = B.tmp(); B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); if (slotTy === "i8") { const wide = B.tmp(); 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; } B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); @@ -648,10 +686,12 @@ export function emitDenseArrayPush( host: LlvmEmitterContext, array: string, acc: ArrayAccess, + element: IrType | null, value: string, ): string { 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}`; host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`); @@ -685,16 +725,20 @@ export function emitDenseArrayPush( B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`); B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`); B.line( - `${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, + `${statePtr} = getelementptr inbounds [${slotWidth(kind)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`, ); host.markMemoryPointer(statePtr, "array:present"); - const valuePtr = valuePointer(host, { offset: len, data }, acc); + const valuePtr = valuePointer(host, { offset: len, data }, kind); if (acc === "f64") { B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else if (acc === "bool") { const bits = B.tmp(); B.line(`${bits} = zext i1 ${value} to i8`); B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } else if (kind === "cref") { + const ref = B.tmp(); + for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } else { B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); } diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..a87903f3a 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -545,7 +545,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ``, `@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`, `declare void @scr_init()`, - ...(emitter.usesCompactRefs ? [`declare void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [`declare void @scr_plr_init()`] : []), `declare void @scr_lib_init(i32, ptr)`, ...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []), ...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []), @@ -840,7 +840,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ` call void @scr_init()`, // Compressed fields hold addresses in the program-lifetime region, which // is reserved below 32 GiB before compiled code runs. - ...(emitter.usesCompactRefs ? [` call void @scr_plr_init()`] : []), + ...(emitter.compactRegion ? [` call void @scr_plr_init()`] : []), // Each context (the main thread, every worker) installs its stack // guard before running compiled code that checks it. ...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []), diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index dc91da2fb..d90a1deac 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -473,7 +473,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.startBlock(pop); const elem = s.arr.type.elem; const acc = elemAccess(elem); - const raw = emitDenseArrayPop(emitter, arr.name, acc, call, call); + 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`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..410db0371 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -641,9 +641,11 @@ export class LlEmitter { * 32-bit references to compact class instances (LlClassMeta.compressed): * loadField/loadRef decode them and storeField/storeRef encode them. */ readonly compressedFieldPtrs = new Set(); - /** Whether a class has compressed fields: main reserves the region before - * compiled code runs (scr_plr_init). */ - usesCompactRefs = false; + /** Compact class instances live in the program-lifetime region below + * 32 GiB (LlvmTargetOptions.lowRegion, and the program has compact + * classes): main reserves it before compiled code runs (scr_plr_init), and + * fields and arrays of compactRefType hold 32-bit references. */ + compactRegion = false; /** Native class fields with different property names cannot occupy the * same slot of one object. Inherited views of the same property share a @@ -1476,6 +1478,8 @@ export class LlEmitter { } } this.shapeHost = { + compactArrayElem: (elem) => this.compactArrayElem(elem), + compactRegion: () => this.compactRegion, sizeType: this.sizeType, cycleBufOffset: this.cycleBufOffset, tracedShapes: this.tracedShapes, @@ -1605,22 +1609,19 @@ export class LlEmitter { ? (cls, i, isCompact) => { const f = cls.fields[i]!; if (this.inlineUnions.classField(cls.name, f.name)) return false; - const t = f.type; - if (t.kind === "object") return isCompact(t.className); - if (t.kind !== "union") return false; - const single = this.nullableUnions.single(t.unionId); - if (single !== null) - return ( - single.nullTag < 0 && - single.arm.kind === "object" && - isCompact(single.arm.className) - ); - const nullable = this.nullableFields.get(cls.name, f.name); + if (this.compactRefType(f.type, isCompact)) return true; + const nullable = + f.type.kind === "union" && this.nullableUnions.single(f.type.unionId) === null + ? this.nullableFields.get(cls.name, f.name) + : null; return nullable !== null && isCompact(nullable.arm.className); } : undefined, ); - this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean)); + this.compactRegion = + mod.lib === undefined && + options.lowRegion === true && + [...this.classMeta.values()].some((m) => m.compact); for (const [name, rec] of RUNTIME_ERROR_CLASSES) { const meta = this.classMeta.get(name); if (!meta) break; // hand-written IR without the builtin defs: no stamps @@ -2232,7 +2233,7 @@ export class LlEmitter { B.condBr(thrown, rejected, clean); B.startBlock(clean); if (acc === "ref") this.toHeap(value); - this.arrPush(values, acc, value.name); + this.arrPush(values, acc, value.name, elem); B.br(next); B.startBlock(rejected); const promise = B.tmp(); @@ -3585,6 +3586,30 @@ export class LlEmitter { return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed }; } + /** Whether values of `t` are NULL or a compact class instance: `C`, + * `C | undefined` or `C | null` for a compact class `C`. `C | null | + * undefined` is not: its null is a sentinel outside the region. */ + compactRefType( + t: IrType, + isCompact: (className: string) => boolean = (name) => + this.classMeta.get(name)?.compact === true, + ): boolean { + if (t.kind === "object") return isCompact(t.className); + if (t.kind !== "union") return false; + const single = this.nullableUnions.single(t.unionId); + return ( + single !== null && + single.nullTag < 0 && + single.arm.kind === "object" && + isCompact(single.arm.className) + ); + } + + /** Whether arrays of `elem` hold 32-bit references (SCR_ELEM_CREF). */ + compactArrayElem(elem: IrType): boolean { + return this.compactRegion && this.compactRefType(elem); + } + /** Loads the reference a field pointer addresses: a pointer, or the * decoded 32-bit reference of a compressed field (compressedFieldPtrs). * `tagged`: the load carries the field's alias tags. */ @@ -4032,7 +4057,7 @@ export class LlEmitter { const acc = boxAccess(t); const cell = B.tmp(); B.line(`${cell} = ${arrNewCall(this.shapeHost, t, "1")} ; TDZ cell`); - this.arrPush(cell, acc === "bool" ? "bool" : "f64", value); + this.arrPush(cell, acc === "bool" ? "bool" : "f64", value, null); this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`); } @@ -6182,8 +6207,8 @@ export class LlEmitter { return tagInSet(this, uName, unionId, tags); } - arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string { - return arrPush(this, arr, acc, value); + arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string { + return arrPush(this, arr, acc, value, element); } emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void { diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 8970aa3be..4ddb5e75d 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -102,13 +102,17 @@ export function tagInSet( return acc; } +/** `element`: the array's element type, or null for arrays whose + * elements are not compact class references (dense-array-access.ts + * slotKind). */ export function arrPush( host: LlvmEmitterContext, arr: string, acc: "f64" | "bool" | "ref", value: string, + element: IrType | null, ): string { - return emitDenseArrayPush(host, arr, acc, value); + return emitDenseArrayPush(host, arr, acc, element, value); } /** Move evaluated values through one bounded slot buffer per call site. */ @@ -116,6 +120,7 @@ export function emitArrayValues( host: LlvmEmitterContext, arr: string, acc: "f64" | "bool" | "ref", + element: IrType, values: LlValue[], prepend = false, sharedBuffer?: string, @@ -129,7 +134,7 @@ export function emitArrayValues( return result; } if (values.length === 1) { - if (!prepend) return arrPush(host, arr, acc, values[0]!.name); + if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element); const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr"; host.declare( `declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`, @@ -628,7 +633,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, vs), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type }; } case "pushSpread": { // `a.push(...src)`: append src's elements in order (borrowed src, @@ -660,7 +665,7 @@ export function emitArrIntrinsic( // from right to left so the final front order is source order. const vs = e.args.map((a) => host.emitExpr(a)); host.emitPublishedArrayGuard(r.name, e.receiver.type, 4, f64Lit(0), f64Lit(vs.length)); - return { name: emitArrayValues(host, r.name, acc, vs, true), type: e.type }; + return { name: emitArrayValues(host, r.name, acc, elem, vs, true), type: e.type }; } case "unshiftSpread": { // The runtime snapshots the borrowed source and handles self-spread. @@ -702,7 +707,7 @@ export function emitArrIntrinsic( let slowLabel = ""; if (method === "pop") { slowLabel = B.newLabel("remove.slow"); - denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel); + denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel); denseLabel = B.currentLabel(); B.br(lp); B.startBlock(slowLabel); @@ -771,7 +776,7 @@ export function emitArrIntrinsic( // by content, everything else by pointer). Strict equality. Arrays of // objects and strings scan inline (array-search.ts). const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const t = B.tmp(); @@ -789,7 +794,7 @@ export function emitArrIntrinsic( // Borrowed needle, SameValueZero (NaN matches NaN). For the inline // searches SameValueZero is strict equality. const v = host.emitExpr(e.args[0]!); - const search = acc === "ref" ? arraySearchKind(elem) : null; + const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null; if (search !== null) { host.arraySearchKinds.add(search); const at = B.tmp(), diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 354004e9e..02a9f971f 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -32,7 +32,7 @@ import { emitPowerOfTwoRemainder, emitSignedIntegerRemainder, } from "./integer-remainder.js"; -import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js"; +import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js"; import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js"; export function emitLiteralExpr( @@ -879,7 +879,7 @@ export function emitContainerExpr( buffer = B.tmp(); B.entryAllocas.push(`${buffer} = alloca [${Math.min(e.elems.length, 64)} x i64]`); } - if (pending.length > 0) emitArrayValues(host, arr, acc, pending, false, buffer); + if (pending.length > 0) emitArrayValues(host, arr, acc, elem, pending, false, buffer); pending = []; }; e.elems.forEach((el, i) => { @@ -924,7 +924,7 @@ export function emitContainerExpr( } const bound = B.tmp(); B.line(`${bound} = fsub double ${n.name}, ${f64Lit(1)}`); - B.countedLoop(bound, () => host.arrPush(arr, acc, fill), "ole"); + B.countedLoop(bound, () => host.arrPush(arr, acc, fill, elem), "ole"); return out; } case "arrayGet": { @@ -962,7 +962,7 @@ export function emitContainerExpr( idx, e.index, "has", - slotWidth(elemAccess(e.arr.type.elem)), + denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } @@ -977,7 +977,7 @@ export function emitContainerExpr( idx, e.index, "state", - slotWidth(elemAccess(e.arr.type.elem)), + denseSlotWidth(host, e.arr.type.elem), ); return { name: t, type: e.type }; } diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 6d5fe652d..7d9be632d 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -163,7 +163,7 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue { B.line(`${row} = call ptr @${mangleRecordNew(rowT.shapeId)}()`); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "0").ptr, STRING, key, false); host.storeField(host.recordFieldPtr(row, rowT.shapeId, "1").ptr, DYN, item, false); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const next = B.tmp(); B.line(`${next} = add ${size} ${i}, 1`); B.line(`store ${size} ${next}, ptr ${iSlot}`); @@ -536,7 +536,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const parent = B.tmp(); B.line(`${parent} = call ptr @scr_fs_glob_parent(ptr ${snap}, ${host.sizeType} ${i}) ; +1`); B.line(`store ptr ${parent}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -620,7 +620,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line( `store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, ); - host.arrPush(arr, "ref", row); // push takes ownership of the row + host.arrPush(arr, "ref", row, null); // push takes ownership of the row const i2 = B.tmp(); B.line(`${i2} = add ${host.sizeType} ${i}, 1`); B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`); @@ -955,7 +955,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.startBlock(lj); const rows = B.tmp(); B.line(`${rows} = load ptr, ptr ${rowsSlot}`); - host.arrPush(rows, "ref", row); // push takes ownership of the row + host.arrPush(rows, "ref", row, null); // push takes ownership of the row B.line(`call void @scr_arr_release(ptr ${rows})`); B.line(`call void @scr_str_release(ptr ${nm})`); const i2 = 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..223d45540 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -15,6 +15,13 @@ import { ReferenceEffects } from "./reference-effects.js"; import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { READ_FALLBACK } from "./dense-array-access.js"; +import { compactRefLoadLines } from "./classes.js"; + +/** The dense slot load of a reference read from an array of type + * `arrayType`: a pointer, or a compact class reference (SCR_ELEM_CREF). */ +function referenceLoad(host: LlvmEmitterContext, arrayType: IrType): "ptr" | "cref" { + return arrayType.kind === "array" && host.compactArrayElem(arrayType.elem) ? "cref" : "ptr"; +} export interface LocalArrayRead { type: IrType; @@ -469,7 +476,7 @@ function emitDenseReferenceArrayRead( no: string, slow: string, join: string, - loadType: "ptr" | "i64" | "i8" = "ptr", + loadType: "ptr" | "cref" | "i64" | "i8" = "ptr", ): void { const B = host.B; const header = (field: number): string => { @@ -555,7 +562,14 @@ function emitDenseReferenceArrayRead( const statePtr = B.tmp(), denseState = B.tmp(), densePresent = B.tmp(); - scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset); + scrArrStateAt( + host, + statePtr, + data, + cap, + loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8, + offset, + ); host.markMemoryPointer(statePtr, "array:present"); B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`); B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`); @@ -564,13 +578,20 @@ function emitDenseReferenceArrayRead( B.startBlock(valueLabel); const valuePtr = B.tmp(), raw = B.tmp(); - // Boolean arrays store one byte per element; every other kind an 8-byte slot. - const slotType = loadType === "i8" ? "i8" : "i64"; + // 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"; B.line( `${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`, ); host.markMemoryPointer(valuePtr, "array:elements"); - B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + if (loadType === "cref") { + const ref = B.tmp(); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart()); + } else { + B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + } present(raw); B.br(join); } @@ -622,6 +643,7 @@ export function emitBorrowedArrayRead( slow, slow, join, + referenceLoad(host, read.arr.type), ); B.startBlock(slow); host.declare("declare ptr @scr_arr_borrow_ref(ptr, double)"); @@ -670,7 +692,7 @@ export function emitLocalArrayRead( no, slow, join, - "ptr", + referenceLoad(host, read.array.type), ); else B.br(slow); B.startBlock(slow); @@ -731,7 +753,7 @@ export function emitLocalArrayRead( no, slow, join, - !scalar ? "ptr" : read.element.kind === "bool" ? "i8" : "i64", + !scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64", ); else B.br(slow); B.startBlock(slow); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f667..22416fa8f 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -71,6 +71,12 @@ 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 arrays of `elem` hold 32-bit references to compact class + * instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */ + compactArrayElem?(elem: IrType): boolean; + /** Whether compact class instances live in the program-lifetime region + * (LlEmitter.compactRegion). */ + compactRegion?(): boolean; /** Byte offset from an object pointer back to ScrCycHdr.buf. */ readonly cycleBufOffset: number; declare(decl: string): void; @@ -1076,18 +1082,30 @@ export function arrNewCall( /** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF * element type, defined once per module and shared by all its arrays. It is - * writable: the runtime stores its registry id on the first array. */ + * 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. */ export function arrOps(host: ShapeHost, elem: IrType): string { const v = vAdapters(host, elem); const trace = traceArg(host, elem); const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_"); const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`; + if (host.compactArrayElem?.(elem) === true) { + host.declare( + `${name}.c = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_COMPACT} }`, + ); + return `${name}.c`; + } 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. */ +const SCR_ARR_OPS_COMPACT = 1; + /* ── capture boxes ────────────────────────────────────────────────────── */ /** Box construction call text (boxNewC's dispatch): plain-kind boxes for diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..a89ae97b8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4638-contract-program-lifetime-arrays.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c00d1b4f6..50b461614 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -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 with its state: an 8-byte value, a boolean byte or + * a 4-byte compact reference. */ 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) + sizeof(uint8_t); } /* Bytes of dense storage (head room included) after the prefix. */ @@ -212,6 +213,11 @@ static bool scr_elem_is_ref(ScrElemKind k) { k == SCR_ELEM_REF; } +/* Whether `a`'s element type comes with element ops (scr_arr_ops). */ +static bool scr_arr_has_ops(const ScrArr *a) { + return a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF; +} + /* Releases an element slot of kind `elem`, with element ops `ops` for * SCR_ELEM_REF. An array's kind and ops never change, so loops look them up * once (element releases may run teardown, which reloads anything read @@ -266,6 +272,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]); return s->data[index]; } @@ -388,9 +395,17 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { } /* Bytes per dense slot: the value and its state byte. Boolean arrays - * store a one-byte value. */ + * store a one-byte value, compact reference arrays a 4-byte one. */ #define SCR_ARR_SLOT_BYTES (sizeof(uint64_t) + sizeof(uint8_t)) #define SCR_ARR_BOOL_SLOT_BYTES (2 * sizeof(uint8_t)) +#define SCR_ARR_CREF_SLOT_BYTES (sizeof(uint32_t) + sizeof(uint8_t)) + +/* SCR_ARR_*_SLOT_BYTES for an element kind. */ +static size_t scr_elem_slot_unit(uint8_t elem) { + return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES + : elem == SCR_ELEM_CREF ? SCR_ARR_CREF_SLOT_BYTES + : SCR_ARR_SLOT_BYTES; +} /* Appends to an array smaller than this grow straight to it. */ #define SCR_ARR_MIN_GROWTH ((size_t)4) @@ -469,7 +484,7 @@ static void scr_arr_drop_head(ScrArr *a) { * for storage without head room (booleans, inline slots); the caller then * moves the tail down itself. */ static bool scr_arr_drop_front(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || 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); @@ -504,18 +519,21 @@ 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 the slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES). */ +static 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) + if (unit == SCR_ARR_BOOL_SLOT_BYTES) return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_BOOL_SLOT_BYTES)) - prefix) / SCR_ARR_BOOL_SLOT_BYTES; + if (unit == SCR_ARR_CREF_SLOT_BYTES) + return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_CREF_SLOT_BYTES)) - prefix) / + SCR_ARR_CREF_SLOT_BYTES; return (scr_sa_class_size(scr_sa_class(prefix + SCR_ARR_SMALL_SLOTS * SCR_ARR_SLOT_BYTES)) - prefix) / SCR_ARR_SLOT_BYTES; #else (void)prefix; - (void)bools; + (void)unit; return SCR_ARR_SMALL_SLOTS; #endif } @@ -528,26 +546,27 @@ 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 the slots of `unit` bytes (SCR_ARR_*_SLOT_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 } -static void scr_arr_use_inline(ScrArr *a, size_t cap) { +/* Points `a` at `cap` inline slots of `unit` bytes (SCR_ARR_*_SLOT_BYTES), + * all holes. */ +static void scr_arr_use_inline(ScrArr *a, size_t cap, size_t unit) { 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); + memset((char *)a->data + cap * (unit - 1), SCR_ARR_HOLE, cap); a->spare |= SCR_ARR_INLINE_SLOTS; } @@ -582,15 +601,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; } @@ -609,14 +628,14 @@ 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->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || - need > SCR_ARR_DENSE_LIMIT) { + if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || + (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; } size_t cap; if (mode == SCR_ARR_GROW_EXACT) { - cap = scr_arr_block_cap(need, false); + cap = scr_arr_block_cap(need, SCR_ARR_SLOT_BYTES); } else { cap = a->cap < SCR_ARR_MIN_GROWTH ? SCR_ARR_MIN_GROWTH : a->cap; while (cap < need) cap += cap; @@ -660,12 +679,11 @@ 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; 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; @@ -723,7 +741,7 @@ static void scr_arr_grow_dense(ScrArr *a, size_t need) { * array's allocation. Returns false when the caller must move the elements * itself. */ static bool scr_arr_open_head(ScrArr *a, size_t count) { - if (a->elem == SCR_ELEM_BOOL || scr_arr_inline(a)) return false; + if (scr_arr_slot_width(a) != sizeof(uint64_t) || scr_arr_inline(a)) return false; size_t head = scr_arr_head(a); if (head < count) { size_t len = a->len; @@ -851,6 +869,20 @@ static inline uint8_t scr_arr_state_at(const ScrArr *a, size_t index, uint64_t * return sparse.state; } +/* scr_arr_state_at for a number[] (SCR_ELEM_F64): 8-byte slots, with no + * element-kind test. */ +static inline uint8_t scr_arr_f64_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_w(a, 8), index); + if (state == SCR_ARR_HOLE) return SCR_ARR_HOLE; + *out = a->data[index]; + return state; + } + ScrArrStateSlot sparse = scr_arr_sparse_state_at(a, index); + if (sparse.state != SCR_ARR_HOLE) *out = sparse.slot; + return sparse.state; +} + 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; } @@ -1148,9 +1180,10 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, if (count == 0) return; scr_arr_cut(dst, to); /* 8-byte slots: a constant slot size, and constant-width state ends. */ - bool wide = dst->elem != SCR_ELEM_BOOL; - 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); + size_t width = scr_arr_slot_width(dst); + bool wide = width == sizeof(uint64_t); + uint8_t *dst_states = wide ? scr_arr_states_end_w(dst, 8) : scr_arr_states_end_w(dst, width); + const uint8_t *src_states = wide ? scr_arr_states_end_w(src, 8) : scr_arr_states_end_w(src, width); 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. */ @@ -1185,21 +1218,28 @@ static void scr_arr_copy_dense(ScrArr *dst, size_t to, const ScrArr *src, /* Store `count` 8-byte value slots into dense storage starting at `to`. */ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t *slots) { - if (a->elem != SCR_ELEM_BOOL) { + if (scr_arr_slot_width(a) == sizeof(uint64_t)) { scr_arr_copy_slots(a->data + to, slots, count); return; } for (size_t i = 0; i < count; i++) scr_arr_dense_put(a, to + i, slots[i]); } +/* The slot unit of an array that scr_arr_new, scr_arr_new_small or + * scr_arr_new_inline makes. Their kinds have no element ops, so they are + * never SCR_ELEM_CREF (scr_arr_new_ops makes those). */ +static size_t scr_arr_plain_slot_unit(ScrElemKind elem) { + return elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES; +} + /* Inlined into each constructor, so `small` and the slot unit fold to * constants there. */ 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_arr_plain_slot_unit(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; @@ -1210,7 +1250,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el a->spare = 0; a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); #ifdef SCR_RC_AUDIT scr_live_arrays++; @@ -1231,7 +1271,7 @@ ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true) ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { if (cap == 0) return scr_arr_make(elem, 0, false); 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_arr_plain_slot_unit(elem); ScrArr *a = scr_mem_alloc(SCR_ARR_INLINE_OFFSET + cap * unit); if (!a) scr_arr_oom(); a->rc = 1; @@ -1242,7 +1282,7 @@ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap) { a->spare = 0; a->filled = 0; a->data = NULL; - scr_arr_use_inline(a, cap); + scr_arr_use_inline(a, cap, unit); #ifdef SCR_RC_AUDIT scr_live_arrays++; #endif @@ -1250,9 +1290,7 @@ 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); + if (scr_arr_inline(a)) scr_arr_move_inline(a, a->cap, scr_arr_slot_unit(a)); /* The swapped fields do not include `spare`: no head room either. */ if (a->spare & SCR_ARR_HEAD_ROOM) { scr_arr_drop_head(a); @@ -1329,18 +1367,20 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } -/* Inlined into each constructor like scr_arr_make: with `small` and the - * count constant, scr_arr_new_ops_small's size class folds to a table - * index. */ -static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, - size_t initial_cap, - bool small) { +/* Inlined into each constructor like scr_arr_make: with `small`, the count + * and `elem` constant, scr_arr_new_ops_small's size class folds to a table + * index and the slot sizes to constants. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const ScrArrOps *ops, + size_t initial_cap, + bool small, + uint8_t elem) { ScrArr *a; bool cyc = ops->trace != NULL; + size_t unit = scr_elem_slot_unit(elem); size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); 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); @@ -1351,7 +1391,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->rc = 1; a->len = 0; a->cap = 0; - a->elem = SCR_ELEM_REF; + a->elem = elem; a->cyc = cyc; /* The ops id is written with the other header fields: reading `spare` * back after scr_arr_use_inline sets its inline flag stalls the @@ -1360,7 +1400,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); a->filled = 0; a->data = NULL; - if (inline_cap) scr_arr_use_inline(a, inline_cap); + if (inline_cap) scr_arr_use_inline(a, inline_cap, unit); else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT); /* After the storage: ops past the registry live in the ext, whose * pointer precedes the dense allocation, so inline slots move out first @@ -1372,6 +1412,16 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrA return a; } +/* SCR_ELEM_CREF for compact element types, SCR_ELEM_REF otherwise. Each + * branch inlines scr_arr_make_ops_kind with a constant kind. */ +static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, + size_t initial_cap, + bool small) { + if (__builtin_expect((ops->flags & SCR_ARR_OPS_COMPACT) != 0, 0)) + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF); + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF); +} + ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { return scr_arr_make_ops(ops, initial_cap, false); } @@ -1676,6 +1726,7 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state, 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 if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot); 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); } else { @@ -1712,7 +1763,7 @@ double scr_math_max_arr(ScrArr *a) { double best = -INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) best = v; @@ -1724,7 +1775,7 @@ double scr_math_min_arr(ScrArr *a) { double best = INFINITY; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) return NAN; double v = scr_slot_to_f64(slot); if (isnan(v)) return v; if (v < best || (v == 0.0 && best == 0.0 && signbit(v))) best = v; @@ -1739,7 +1790,7 @@ double scr_math_hypot_arr(ScrArr *a) { bool has_nan = false; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) { + if (scr_arr_f64_state_at(a, i, &slot) != SCR_ARR_VALUE) { has_nan = true; continue; } @@ -1754,7 +1805,7 @@ double scr_math_hypot_arr(ScrArr *a) { double correction = 0.0; for (size_t i = 0; i < a->len; i++) { uint64_t slot; - (void)scr_arr_state_at(a, i, &slot); + (void)scr_arr_f64_state_at(a, i, &slot); double v = scr_slot_to_f64(slot) / scale; double term = v * v - correction; double next = sum + term; @@ -1830,9 +1881,9 @@ void *scr_arr_peek_ref(const ScrArr *a, double i) { if (i >= 0 && i < (double)a->cap) { 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->filled) return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); if (idx >= a->len || *scr_arr_state_ptr(a, idx) != SCR_ARR_VALUE) return NULL; - return scr_slot_to_ptr(a->data[idx]); + return scr_slot_to_ptr(scr_arr_dense_get(a, idx)); } } uint64_t slot; @@ -1886,7 +1937,7 @@ void *scr_arr_get_ref(ScrArr *a, double i) { if (a->elem == SCR_ELEM_STR) scr_str_retain((ScrStr *)p); else if (a->elem == SCR_ELEM_BYTES) scr_bytes_retain((ScrBytes *)p); else if (a->elem == SCR_ELEM_REF) p = scr_arr_ops(a)->retain(p); - else scr_arr_retain((ScrArr *)p); + else if (a->elem == SCR_ELEM_ARR) scr_arr_retain((ScrArr *)p); return p; } @@ -2395,7 +2446,7 @@ ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount) { * otherwise the range may be mostly holes, so the capacity is a hint. */ size_t hint = scr_arr_is_dense(a) || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), hint ? hint : 1) : scr_arr_new(a->elem, hint ? hint : 1); out->len = n; @@ -2475,7 +2526,7 @@ 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; - ScrArr *removed = a->elem == SCR_ELEM_REF + ScrArr *removed = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), n) : scr_arr_new(a->elem, n); if (n) { @@ -2521,7 +2572,7 @@ void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t cou size_t next_len = a->len - n + count; bool refs = scr_elem_is_ref(a->elem); if (!scr_arr_is_dense(a) || !scr_arr_dense_ok(a, next_len, count)) { - ScrArr *items = a->elem == SCR_ELEM_REF + ScrArr *items = scr_arr_has_ops(a) ? scr_arr_new_ops(scr_arr_ops(a), count) : scr_arr_new(a->elem, count); scr_arr_push_many(items, count, slots); @@ -2636,7 +2687,8 @@ 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); 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_state_in(states, i) == SCR_ARR_VALUE && scr_arr_ref_eq(a, scr_arr_dense_get(a, 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; @@ -2727,11 +2779,12 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { * sparse side store may be mostly holes, so its capacity is a hint. */ size_t hint = to <= a->cap || n <= SCR_ARR_DENSE_LIMIT ? n : SCR_ARR_DENSE_LIMIT; ScrArr *out = - a->elem == SCR_ELEM_REF + scr_arr_has_ops(a) ? 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)) { + if (to <= a->cap && to <= a->filled && scr_arr_slot_width(a) == sizeof(uint64_t) && + !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); @@ -3024,14 +3077,14 @@ static ScrSortCompare scr_sort_compare_for(const ScrArr *a, ScrClosure *f, uint3 bool multi = order >= 2; if (order != 0 && (arity != 2 || (order != 1 && order != -1 && !scr_sort_keys_valid(order)) || - (c.numbers ? key != NULL || multi : a->elem != SCR_ELEM_REF || key == NULL))) + (c.numbers ? key != NULL || multi : !scr_arr_has_ops(a) || key == NULL))) scr_trap("scriptc: invalid keyed sort\n"); if (!c.numbers) { c.borrowing = f->fn_borrowed != NULL; if (a->elem == SCR_ELEM_STR) c.retain = scr_sort_retain_str; else if (a->elem == SCR_ELEM_ARR) c.retain = scr_sort_retain_arr; else if (a->elem == SCR_ELEM_BYTES) c.retain = scr_sort_retain_bytes; - else if (a->elem == SCR_ELEM_REF) c.retain = scr_arr_ops(a)->retain; + else if (scr_arr_has_ops(a)) c.retain = scr_arr_ops(a)->retain; else scr_trap("scriptc: invalid comparator sort element kind\n"); } return c; @@ -4619,7 +4672,7 @@ static bool scr_arr_dense_values(const ScrArr *a) { * elements (stored in 1-byte slots). The slots are borrowed and stay valid * while nothing writes the array. */ const uint64_t *scr_arr_dense_slots(const ScrArr *a, size_t *len) { - if (a->len == 0 || a->elem == SCR_ELEM_BOOL || !scr_arr_dense_values(a)) return NULL; + if (a->len == 0 || scr_arr_slot_width(a) != sizeof(uint64_t) || !scr_arr_dense_values(a)) return NULL; *len = a->len; return a->data; } @@ -4646,7 +4699,9 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in ScrClosure *key) { uint8_t elem = a->elem; const ScrArrOps *ops = scr_arr_ops(a); - if (a->rc == SIZE_MAX || !scr_arr_dense_values(a)) return false; + /* Compact references sort through the snapshot path, which reads and + * writes 8-byte slots. */ + if (a->rc == SIZE_MAX || elem == SCR_ELEM_CREF || !scr_arr_dense_values(a)) return false; size_t n = a->len; if (n < 2) return true; if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); @@ -4687,12 +4742,14 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in } 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. */ + * yet written. The kind, and so the slot width, never changes. */ + size_t width = scr_arr_slot_width(a); for (size_t i = 0; i < n; i++) { - bool held = *scr_arr_state_ptr(a, i) == SCR_ARR_VALUE; + uint8_t *state = scr_arr_state_in(scr_arr_states_end_w(a, width), i); + bool held = *state == 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; + *state = SCR_ARR_VALUE; if (held) scr_elem_release(a, old); } } @@ -4787,6 +4844,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { case SCR_ELEM_ARR: case SCR_ELEM_BYTES: case SCR_ELEM_REF: + case SCR_ELEM_CREF: /* The compiler rejects join on ref-element arrays (SC1090). */ scr_trap("scriptc: internal error: join on a ref-element array\n"); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 6c41b3dbd..2fea1ef2c 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -43,6 +43,11 @@ typedef enum { * constant descriptor per element type, stored once per array; every * array is still monomorphic (one element type, one set of entry points). */ SCR_ELEM_REF, + /* An SCR_ELEM_REF type whose elements are compact class instances in the + * program-lifetime region (SCR_ARR_OPS_COMPACT): 4-byte slots holding the + * address over 8, 0 for NULL. Compact instances are never counted or + * traced, so these arrays retain, release and trace nothing. */ + SCR_ELEM_CREF, } ScrElemKind; typedef enum { @@ -63,8 +68,14 @@ typedef struct ScrArrOps { /* Registry id (scr_arr_ops_table), 0 until scr_arr_ops_register assigns * it on the first array of the type. Descriptors are writable for it. */ uint32_t id; + /* SCR_ARR_OPS_* */ + uint32_t flags; } ScrArrOps; +/* The elements are compact class instances in the program-lifetime region: + * arrays of the type are SCR_ELEM_CREF (llvm/shapes.ts arrOps). */ +#define SCR_ARR_OPS_COMPACT 1u + 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); @@ -84,7 +95,7 @@ typedef struct ScrArr { /* 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 * registry id of the element type's ops (scr_arr_ops), 0 for every kind - * but SCR_ELEM_REF. */ + * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; /* A lower bound on the hole-free prefix: filled <= len, filled <= cap, * and every dense state below it is SCR_ARR_VALUE. Element reads below @@ -93,10 +104,12 @@ typedef struct ScrArr { * undefined) or move states below it lowers it first (scr_arr_cut); * 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 - * slots for every kind; a bool slot is 0 or 1 wherever it lives. Rarely + /* 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 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 (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 * (SCR_ARR_HAS_EXT) keeps a pointer to it in the word before the dense @@ -190,16 +203,24 @@ static inline uint32_t scr_arr_ops_id(const ScrArrOps *ops) { /* The element ops of an array whose ops have no registry id. */ const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a); -/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF. */ +/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF and + * SCR_ELEM_CREF. */ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) { uint32_t id = (uint32_t)a->spare >> SCR_ARR_OPS_SHIFT; if (__builtin_expect(id == SCR_ARR_OPS_OVERFLOW, 0)) return scr_arr_ops_overflow(a); return scr_arr_ops_table[id]; } +/* 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); + return sizeof(uint64_t); +} + /* 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); + return scr_elem_slot_width(a->elem); } /* The dense states, one byte per slot, share the values' allocation (inline @@ -233,14 +254,23 @@ 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; } +/* A CREF slot's pointer, and the slot of a pointer (NULL or a compact + * instance below 32 GiB). */ +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 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]); return a->data[index]; } 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 a->data[index] = slot; } diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index df4da1738..ee0bb055a 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -22,7 +22,7 @@ static bool copying_elem_is_ref(ScrElemKind kind) { } static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) { - ScrArr *out = a->elem == SCR_ELEM_REF + ScrArr *out = a->elem == SCR_ELEM_REF || a->elem == SCR_ELEM_CREF ? scr_arr_new_ops(scr_arr_ops(a), cap ? cap : 1) : scr_arr_new(a->elem, cap ? cap : 1); out->len = cap; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 4be596ab5..37ca28f16 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -13002,7 +13002,7 @@ ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { 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_dense_put(out, out->len, scr_map_entry_at(s, i)->key); *scr_arr_state_in(states, out->len++) = SCR_ARR_VALUE; } return out; diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 675a13ca9..4ec993055 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -199,6 +199,19 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { if (fn) scr_pub_push(ctx, p, fn); } +/* The elements of a compact reference array (SCR_ELEM_CREF): 4-byte dense + * slots, 8-byte pointers in the sparse entries. */ +static __attribute__((noinline)) void scr_pub_cref_elems(ScrArr *a, ScrPub *ctx, ScrPubFn fn) { + size_t dense = a->len < a->cap ? a->len : a->cap; + const uint32_t *refs = (const uint32_t *)a->data; + const uint8_t *states = scr_arr_states_end_w(a, sizeof(uint32_t)); + for (size_t i = 0; i < dense; i++) + if (!states || *scr_arr_state_in(states, i)) scr_pub_slot(ctx, scr_cref_decode(refs[i]), fn); + if (scr_arr_ext_of(a)) + for (size_t i = 0; i < scr_arr_ext_of(a)->sparse_len; i++) + if (scr_arr_ext_of(a)->sparse[i].state) scr_pub_slot(ctx, scr_arr_ext_of(a)->sparse[i].slot, fn); +} + void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; if (scr_arr_ext_of(a) && (scr_arr_ext_of(a)->props || scr_arr_ext_of(a)->metadata)) { @@ -206,10 +219,15 @@ void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { return; } if (!scr_pub_mark(ctx, a, a->cyc != 0, SCR_PUB_KIND_ARRAY)) return; + if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0)) { + scr_pub_cref_elems(a, ctx, elem); + return; + } ScrPubFn fn = a->elem == SCR_ELEM_STR ? scr_pub_str_fn : a->elem == SCR_ELEM_REF ? elem : NULL; if (!fn) return; + /* String and reference elements have 8-byte slots. */ 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_states_end_w(a, sizeof(uint64_t)); 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/tests/corpus/4638-contract-program-lifetime-arrays.ts b/tests/corpus/4638-contract-program-lifetime-arrays.ts new file mode 100644 index 000000000..a50094f8b --- /dev/null +++ b/tests/corpus/4638-contract-program-lifetime-arrays.ts @@ -0,0 +1,117 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// Arrays of ProgramLifetime class instances: literals, growth, removal +// from both ends, splicing, slicing, concatenation, searching, sorting, +// filling, copying methods, holes, length changes, Sets and Maps built +// from them, arrays held in class fields, optional elements, inspection +// and JSON. Each operation sees the same instances, in the same order. + +class Sym implements ProgramLifetime { + readonly name: string; + readonly rank: number; + readonly decls: Sym[] = []; + parent: Sym | undefined = undefined; + constructor(name: string, rank: number) { + this.name = name; + this.rank = rank; + } +} + +class Scope implements ProgramLifetime { + readonly symbols: Sym[] = []; + readonly optional: (Sym | undefined)[] = []; + readonly nullable: (Sym | null)[] = []; +} + +const names = (xs: readonly (Sym | undefined | null)[]): string => + xs.map((s) => (s === undefined ? "u" : s === null ? "n" : s.name)).join(","); + +const a = new Sym("a", 3); +const b = new Sym("b", 1); +const c = new Sym("c", 2); +const d = new Sym("d", 5); +const e = new Sym("e", 4); + +// Literals, push, pop, shift and unshift. +const xs: Sym[] = [a, b, c]; +xs.push(d); +xs.push(e, a); +console.log(names(xs), xs.length); +console.log(xs.pop()?.name, xs.shift()?.name, names(xs)); +xs.unshift(e, d); +console.log(names(xs), xs[0] === e, xs.at(-1) === e); + +// Growth well past the inline slots, then reads by index. +const many: Sym[] = []; +for (let i = 0; i < 1000; i++) many.push(i % 2 === 0 ? a : new Sym(`s${i}`, i)); +let ranks = 0; +for (let i = 0; i < many.length; i++) ranks += many[i]!.rank; +console.log(many.length, ranks, many[999]!.name, many[998] === a); + +// Splice, slice, concat, spread and copying methods. +const ys = xs.slice(1, 4); +const removed = xs.splice(1, 2, a, b, c); +console.log(names(ys), names(removed), names(xs)); +const zs = [...ys, ...removed].concat(xs); +console.log(names(zs), zs.length); +console.log(names(zs.toReversed()), names(zs.with(0, c)), names(zs.toSpliced(1, 3))); + +// Searching by identity. +console.log(zs.indexOf(c), zs.lastIndexOf(c), zs.includes(d), zs.indexOf(new Sym("c", 2))); +console.log(zs.find((s) => s.rank > 3)?.name, zs.findIndex((s) => s === b), zs.some((s) => s === e)); +console.log(zs.every((s) => s.rank > 0), zs.filter((s) => s.rank % 2 === 1).length); + +// Sorting with comparators, reversing and filling. +const sorted = [...zs].sort((p, q) => p.rank - q.rank || p.name.localeCompare(q.name)); +console.log(names(sorted)); +console.log(names(zs.toSorted((p, q) => q.name.localeCompare(p.name)))); +sorted.reverse(); +console.log(names(sorted)); +const filled: Sym[] = new Array(5).fill(b); +filled.fill(c, 2, 4); +console.log(names(filled), filled.copyWithin(0, 3).map((s) => s.name).join("")); + +// Holes, length changes and iteration. +const holey: Sym[] = [a, b]; +holey[5] = c; +console.log(holey.length, 2 in holey, holey[5]?.name, holey.findIndex((s) => s === c)); +holey.length = 3; +console.log(holey.length, holey[5]); +let visited = ""; +for (const s of xs) visited += s.name; +xs.forEach((s, i) => { + visited += i; +}); +console.log(visited, names(xs.map((s) => s.parent ?? s))); + +// Arrays inside class instances, optional and nullable elements. +const scope = new Scope(); +for (const s of [a, b, c, d, e]) { + scope.symbols.push(s); + s.decls.push(s, a); + s.parent = scope.symbols[0]; +} +scope.optional.push(a, undefined, c); +scope.nullable.push(null, b, null); +scope.optional[3] = e; +console.log(names(scope.symbols), names(scope.optional), names(scope.nullable)); +console.log(scope.optional.indexOf(undefined), scope.nullable.indexOf(null), scope.optional.length); +console.log(c.decls.length, c.decls[1] === a, e.parent === a, names(e.decls)); + +// Sets and Maps built from arrays, and arrays built from them. +const set = new Set(zs); +console.log(set.size, set.has(d), names([...set])); +const byName = new Map(zs.map((s) => [s.name, s] as const)); +console.log(names([...byName.values()]), byName.get("b") === b); +const grouped = new Map(); +for (const s of many.slice(0, 10)) { + const list = grouped.get(s.rank % 3) ?? []; + list.push(s); + grouped.set(s.rank % 3, list); +} +console.log([...grouped.entries()].map(([k, v]) => `${k}:${v.length}`).join(" ")); + +// Inspection and JSON. +console.log(xs.slice(0, 2).map((s) => s.name)); +console.log(JSON.stringify(scope.nullable.map((s) => s?.rank ?? null))); +console.log(JSON.stringify([a, b].map((s) => ({ name: s.name, rank: s.rank })))); diff --git a/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json new file mode 100644 index 000000000..8512b117c --- /dev/null +++ b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4600 } From a6a1eb2b95c7b2c4d1ea436526151aba44a110c1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:57:53 -0500 Subject: [PATCH 053/191] 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 054/191] 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 055/191] 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 056/191] 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 9aaebbab254ac0ee6359e10eca248fefab0c3559 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:14:27 -0500 Subject: [PATCH 057/191] Prepare v0.4.2 release --- CHANGELOG.md | 156 +++++++++++++++++- packages/cli-darwin-arm64/package.json | 2 +- packages/cli-darwin-x64/package.json | 2 +- packages/cli-linux-arm64-gnu/package.json | 2 +- packages/cli-linux-arm64-musl/package.json | 2 +- packages/cli-linux-x64-gnu/package.json | 2 +- packages/cli-linux-x64-musl/package.json | 2 +- packages/cli-win32-x64-msvc/package.json | 2 +- packages/cli/package.json | 2 +- packages/compiler/package.json | 2 +- packages/compiler/surface-manifest.json | 2 +- packages/experimental/package.json | 2 +- packages/llvm-darwin-arm64/package.json | 2 +- packages/llvm-darwin-x64/package.json | 2 +- packages/llvm-linux-arm64-gnu/package.json | 2 +- packages/llvm-linux-arm64-musl/package.json | 2 +- packages/llvm-linux-x64-gnu/package.json | 2 +- packages/llvm-linux-x64-musl/package.json | 2 +- packages/llvm-win32-x64-msvc/package.json | 2 +- packages/runtime-android-arm64/package.json | 2 +- packages/runtime-darwin-arm64/package.json | 2 +- packages/runtime-darwin-x64/package.json | 2 +- packages/runtime-ios-arm64/package.json | 2 +- .../runtime-ios-simulator-arm64/package.json | 2 +- packages/runtime-linux-arm64-gnu/package.json | 2 +- .../runtime-linux-arm64-musl/package.json | 2 +- packages/runtime-linux-x64-gnu/package.json | 2 +- packages/runtime-linux-x64-musl/package.json | 2 +- packages/runtime-wasm32-wasi/package.json | 2 +- packages/runtime-win32-x64-msvc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/threads/package.json | 2 +- packages/types/package.json | 2 +- 33 files changed, 186 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d34e8db..2bdd47363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,160 @@ All notable changes to scriptc will be documented in this file. +## 0.4.2 + +### Highlights + +- **Faster builds of large programs:** + - scriptc checks with tsc-ts 0.0.7, which answers contextual types and resolved signatures in batches, so lowering asks for them in one request per chunk instead of one per node. A native build of tsc-ts is 7% faster (110.5 → 103.5 s on 8 vCPUs, lowering 12% faster), with an identical executable. + - The native compiler is built with profile-guided optimization, so tsc-ts builds 6.9% faster cold and 10.7% faster after an edit. + - The class dispatch passes index their receivers by name instead of testing each one, and they reuse their per-function scans while a function's body is unchanged. + - Checker replies resolve objects the compiler already holds from their ids without decoding them. +- **Leaner builds:** + - The frontend of an optimized executable's first build runs in a child process, so the native code generator starts after its memory is released. The cold build of tsc-ts peaks at 2.21 GB instead of 3.30 GB. + - The compiler collects its heap as lowering and LLVM emission grow it. +- **WebCrypto:** + - The `crypto` global, `node:crypto`'s `webcrypto` and `crypto.subtle` are one object. + - `generateKey`, `importKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, `deriveKey`, `wrapKey` and `unwrapKey` cover HMAC, AES-GCM, AES-CBC, AES-CTR, AES-KW, PBKDF2, HKDF and JSON Web Keys. + - `crypto.subtle` signs and verifies with ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519. + - All of it follows Node 24's argument conversion, algorithm normalization and `DOMException`s. jose's symmetric JWE paths run as in Node. +- **JSX:** `.tsx`, `.jsx` and JavaScript files with JSX compile under the project's `jsx` options. + - `react`, `react-jsx` and `react-jsxdev` lower to the factory calls TypeScript emits, with the same keys, fallbacks and source positions. + - `preserve` and `react-native` are refused with a named diagnostic. +- **Faster worker messages:** + - A message is encoded into one block sized to its contents. + - A typed record is posted from its fields. + - A `'message'` listener with a typed parameter builds it from the message, without a checked-dynamic copy. + - Messages between `MessageChannel` ports take 90 ms where they took 250 ms in 0.4.1 (Node: 337 ms), with a peak RSS of 45 MB instead of 115 MB. +- **Loops over typed arrays vectorize:** a loop to a typed array's length keeps one bounds test for every typed array it indexes. + - Index temporaries such as `const j = i * 4` are computed as integers. + - Uint8ClampedArray stores convert inline. + - Row-major index terms such as `r * cols + j` are tested once before the loop. + - Typed-array callback methods call a literal callback directly on an integer counter. + - An 896 × 896 matrix-vector product with four accumulators takes 55 ms instead of 396 ms (Node: 275 ms). `Uint8Array.map` is 4.5 times as fast and `Int32Array.some` 7.8 times as fast as in 0.4.1. + +### Features + +- **Asymmetric keys and signatures in `node:crypto`:** + - `generateKeyPair`, `generateKeyPairSync`, `createPrivateKey` and `createPublicKey` handle RSA, EC and Ed25519 keys in PEM, DER and JWK. + - `sign`, `verify`, `createSign` and `createVerify` sign and verify with those keys, with Node's output and errors. + - Ed25519 verification matches Node on 2,083 test vectors, including small-order and mixed-order keys. +- **`crypto.createDiffieHellman`, `crypto.getDiffieHellman` and `crypto.createDiffieHellmanGroup`:** + - The modp1, modp2 and modp5 to modp18 groups, explicit primes and generators, and generated safe primes are supported, with Node 24's key widths, `verifyError` bits, key range checks and errors. + - Secrets of 0, 1 and p − 1 are rejected, and so are peer keys outside the RFC 5114 subgroups. + - Primes longer than 8192 bits are refused. +- **TLS:** + - `tls.connect` and `tls.createServer` negotiate `ALPNProtocols`, and both sockets report `alpnProtocol`. `minVersion` and `maxVersion` bound the handshake, and `getProtocol()` reports the version it picked. + - `tls.createServer` takes an `SNICallback` that answers a `SecureContext` synchronously or later. + - `new tls.TLSSocket(socket, { isServer: true, ... })` and `tls.connect({ socket })` run the handshake on an open socket, such as an HTTP `CONNECT` tunnel. + - A client whose peer closes before the handshake completes fails with Node's `ECONNRESET`. +- **Streams:** + - Readable, Writable, Duplex, Transform and PassThrough streams take `objectMode`, `readableObjectMode` and `writableObjectMode`, and an object-mode side carries any value through `push`, `write`, `pipe`, `pipeline`, `for await` and `'data'` listeners. + - `_write` and `_transform` receive the encoding argument as Node passes it. + - `Cipheriv` and `Decipheriv` are Transforms: they can be piped, used as `pipeline()` stages, read and iterated. + - `pipeline()` accepts function stages anywhere among its streams, and an array, a string or a synchronous generator as its source. +- **Packages compiled from their JavaScript:** + - js-yaml and smol-toml compile statically and match Node: ES5 error constructors (`Object.create(Error.prototype)`, `Error.call(this)`, `Error.captureStackTrace`) and classes extending `Date` behave as in Node. + - TypeScript's UMD module wrapper compiles as the module body, and tsc's `__importDefault` and `__importStar` helpers around builtin modules compile as plain `require()` calls. + - Interfaces, type aliases and enums that only a package's `.d.ts` declares keep their types. +- **JavaScript sources:** + - Error constructors can be read as values, so `err.constructor === TypeError` works. + - `workerData` can be destructured. + - Unary `+` converts unions of strings, numbers, booleans, `null` and `undefined` as `Number()` does, so `+process.argv[2]` compiles. + - These compile and behave as in Node: + - chained readline listener registration; + - `Number.parseInt` without a radix; + - `match` and `search` with a possibly undefined regular expression; + - `Buffer.from` over an untyped `ArrayBuffer`, with its byte offset; + - Atomics on untyped arrays. + - Buffer methods run on Buffers held as untyped values. +- **`fs`:** + - The synchronous functions (`readFileSync`, `writeFileSync`, `statSync`, `readdirSync`, `mkdirSync`, `rmSync`, `openSync` and the others) accept `file:` URLs wherever they accept a path, with Node's conversion errors. + - `fs.promises.rm` takes `recursive`, `force`, `maxRetries` and `retryDelay`, and removing a directory without `recursive` throws Node's `ERR_FS_EISDIR`. +- **`TypedArray.of`** creates a typed array of its arguments, and a dynamic iterable spread into `Array.of`, `push` or `TypedArray.of` is drained through the iterator protocol. +- **Global `isFinite` converts its argument with ToNumber,** as `isNaN` does. The Math functions and `String.fromCharCode` evaluate every argument before converting any. +- **`Object.freeze`, `Object.seal` and `Object.preventExtensions` of a function** act on its own properties, and `Object.isFrozen`, `Object.isSealed` and `Object.isExtensible` answer for functions. @noble/hashes' frozen hashers work. +- **EventEmitters used through both typed and untyped references** share one listener list. An untyped `emit` reaches typed listeners, an untyped `emit('error')` follows Node's contract, and `listenerCount`, `eventNames`, `listeners`, `rawListeners`, `removeAllListeners`, `setMaxListeners` and `getMaxListeners` work on untyped values. +- **Template literals, `join`, `Array.prototype.toString` and the default `sort()`** call an object's own or inherited `toString`, as `String()` does, instead of printing `[object Object]`. +- **Classes and generics:** + - A generic factory declared to return a constructor type can return one of its local classes, including an anonymous class expression, and `class X extends factory(...)` extends it. + - Static methods of a function-local class compile. + - Constructor types declared in the program (`new () => unknown`, types that also name static members, construct overloads) hold class values. + - Members typed by a conditional type over a literal class type argument (`Field`) resolve. + - Object literals under an erased generic member may hold callbacks with optional or defaulted parameters. +- **Assignment and update targets wrapped in parentheses or non-null assertions** (`(x) = 1`, `h.sums[k]! += 10`) compile. +- **Untyped values keep what JavaScript code adds:** + - Properties that untyped code adds to a typed object literal stay on it. + - Named properties written to an array through an `unknown` view stay on the array, and `for...in` visits them after the indices. + - An `unknown` value asserted to a structural type with methods keeps its class's methods. + - The type inferred from an empty object literal holds any non-nullish value, as TypeScript's `{}` does. +- **A WebAssembly module the build finds but does not translate** rejects with a `CompileError` that names the reason, for example a proposal outside the translated feature set. +- **SC6101 names the functions that likely run often and use untyped values:** + - It shows up to five, most likely hot first, with why their values are untyped: an `any`, an untyped `JSON.parse` result or an untyped JavaScript file. + - `scriptc coverage --fail-on=dynamic` fails on it, and `--no-dynamic-tier-warning` leaves it out. + +### Performance + +- **Regular expressions skip start positions that their leading character sets rule out,** testing 16 positions at once. Searching 50 MB of text for nine case-insensitive alternations takes 0.83 s instead of 3.88 s (Node: 4.38 s). +- **Loop counters stay integers in more loops:** + - Counters that step beside a counted loop's counter (`for (let i = 0, p = 0; i < n; i++, p += 4)`) stay integers: a pixel-stride loop takes 114 ms instead of 340 ms (Node: 190 ms). + - So do two counters that close in on each other (`lo++, hi--`), 17% faster on a permutation-reversal loop. + - So do counters of loops bounded by a string's length or by an invariant limit other than an array's length, and while-loop counters that nested scans advance. A non-ASCII lexer is 9.5% faster. +- **Loops that call a known callback** test its closure once before the loop and call the function directly, so captured `let` bindings stay in registers. `forEach` sums with a captured accumulator are 25 to 55% faster. +- **A speculative integer loop whose check fails** finishes the iteration outside the ordinary loop, which stays optimizable, and a loop site that keeps failing stops trying. With the hole-free-prefix test that element reads now take first, a pairwise loop over an array of class instances with number fields is about 1.25 times as fast as in 0.4.1. +- **Typed arrays over a SharedArrayBuffer** read and write elements inline as relaxed atomic accesses. A numeric kernel over shared `Float64Array`s takes 0.78 s instead of 1.56 s (Node: 0.84 s). +- **`Math.pow(2, n)` for integer exponents** builds the result from its bits. A counter that masks with `Math.pow(2, length)` takes 11.0 s instead of 20.4 s. +- **Async calls that never suspend run in place,** and so does `await Promise.all` of such calls, with Node's microtask turns and rejection handling. An async pipeline takes 5.5 ms instead of 9.7 ms. +- **Strings:** + - `Number()` of a short integer string returns the integer directly. + - `split` builds its result in one inline block, and `indexOf` and `slice` on ASCII strings skip the UTF-16 index map. A log summary is 20% faster. + - `charAt` and `s[i]` on ASCII strings read the character from a table. + - Chains of one-character `replace` calls (the HTML-escape idiom) replace a block of 16 bytes at a time, 12.8% faster on a template renderer. +- **`JSON.parse` and `JSON.stringify`** scan whole words and append parsed typed arrays in bulk, 22% faster to parse and 10% faster to stringify records. +- **Smaller arrays, maps and program-lifetime instances:** + - The array header is 32 bytes instead of 48. + - Arrays of up to 16 known elements keep their slots in their own block. + - Arrays of a known final length get exactly that capacity. + - Small map entry arrays grow in finer steps. + - On Linux, `ProgramLifetime` instances take a 32-bit header and are referenced in 4 bytes. + - They lower the peak memory of a large TypeScript type checker: the 4-byte references alone take a large project's check from 4.13 GB to 3.75 GB, and the 32-bit header saves a further 5%. +- **Worker messages queued together are delivered in one batch** before immediates run. 20,000 messages interleaved with a worker's output drain in 0.5 s instead of 5.6 s (Node: 0.57 s). +- **The cycle collector** never buffers instances of classes whose fields hold nothing cycle-capable, and its per-object paths are shorter. A workload of cyclic graphs is about 5% faster. +- **Array helpers specialized for a callback that cannot throw** skip the pending-exception test, 4.5% faster on a functional pipeline. +- **Compile time:** + - A file's function declarations are walked once for their references, so lowering a 10 MB single-module bundle takes 69 s instead of 219 s. + - LLVM emission computes its module facts once, 6.9% faster on tsc-ts. + - AST walks visit subtrees by their node ranges and read node words through a `Uint32Array`. + - Program builds no longer import runtime bitcode, so builds through Node and through the native compiler produce identical executables. + +### Fixes + +- **Member reads and method calls on an array hole** held in a binding narrowed to one arm of a wide union throw Node's `Cannot read properties of undefined` TypeError. +- **`AsyncLocalStorage`:** + - A store entered before an async function's first await, or in a body that never suspends, is the caller's store after the call. + - Each synchronous generator step runs in its resumer's context, as in Node. +- **Generic arrow and function-expression bindings used before their declarations** behave as in Node, including the `ReferenceError` of a `let` or `const`. +- **Assignment expressions into a module `let` declared without an initializer** (`new Promise((r) => (resolve = r))`) store the value instead of throwing a `ReferenceError`. +- **The `extends` expression of a class defined inside a function** is evaluated each time the definition runs. +- **A `super()` call that leads an `if` condition's comma chain** compiles. +- **A catch binding tests `instanceof`** correctly for values thrown as `unknown`, rejected promises and runtime errors. +- **zlib callbacks receive `undefined` as their result when the operation fails,** as in Node, and a callback may assign a fallback to it. +- **`Readable.push` and `Writable.write` with `string | null` or `Buffer | null` chunks** no longer crash. +- **Object literals keep their key order** through compiler rewrites and on shapes a typed `JSON.parse` also builds, so `JSON.stringify`, `Object.keys` and `console.log` match Node. +- **`console.log` prints class instances with their class name** in programs that test `instanceof` on unknown values. +- **`String`, `Number` and `Boolean` passed to array methods** convert as their direct calls do, and nested array literals in a recursive JSON-style union build. +- **A dynamic method call that the program also reads through a prototype** finds the method when the instance's property bag does not hold it. +- **Storing an element on `Array.prototype`** fails with SC2020, because holes in native arrays do not read inherited elements. +- **A recursive type alias in a generic signature** no longer crashes the compiler. +- **A `Buffer` view of an untyped typed array** compiles. +- **JSX programs checked with tsgo** compile as they do with tsc-ts. +- **Two programs whose entries share a name and build into one directory** no longer share a cached translation unit, so a later cache hit always relinks the right program. +- **A warm build whose cached LLVM exceeds the engine's string limit** reads it in parts instead of failing with `Invalid string length`. +- **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it. +- **Libraries that use untyped byte arrays** link. + + + ## 0.4.1 ### Highlights @@ -77,8 +231,6 @@ All notable changes to scriptc will be documented in this file. - **TypeScript enum exports in compiled packages stay `undefined` until they are assigned,** as in Node. - **Element writes accept a possibly undefined index** (typed-array writes, compound and update writes, element assignments used as values and destructuring targets) and convert it with ToNumber, as element reads already did. - - ## 0.4.0 ### Breaking changes diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 482e5f1e5..23584b60a 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index b1ced2c03..d9e6dafc6 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for darwin-x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index 0aa213279..5b8cb4db2 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index 4ac5434ae..0901602ec 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-arm64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index 7473c36bc..f01478aad 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-gnu", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index 08e048cd1..a6986a66a 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for linux-x64-musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index 54e323839..6b03811b8 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/cli-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Native scriptc compiler for win32-x64-msvc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/cli/package.json b/packages/cli/package.json index d66ba3247..034747c29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "scriptc", - "version": "0.4.1", + "version": "0.4.2", "description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/compiler/package.json b/packages/compiler/package.json index a283f821a..a1d66963d 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/compiler", - "version": "0.4.1", + "version": "0.4.2", "description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 45b3edbaa..637a5f831 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.4.1", + "compilerVersion": "0.4.2", "coverage": [ "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 5b6906de6..cfb4151a4 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.4.1", + "version": "0.4.2", "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-arm64/package.json b/packages/llvm-darwin-arm64/package.json index 472056c39..615fe9d81 100644 --- a/packages/llvm-darwin-arm64/package.json +++ b/packages/llvm-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json index 7f55cb6b5..fd6caf976 100644 --- a/packages/llvm-darwin-x64/package.json +++ b/packages/llvm-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json index 0e1a55b22..7d66257f4 100644 --- a/packages/llvm-linux-arm64-gnu/package.json +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json index efdd97444..b3a01089d 100644 --- a/packages/llvm-linux-arm64-musl/package.json +++ b/packages/llvm-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json index 39667d535..0b3264ec7 100644 --- a/packages/llvm-linux-x64-gnu/package.json +++ b/packages/llvm-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json index 9efd41845..6c83d1cb9 100644 --- a/packages/llvm-linux-x64-musl/package.json +++ b/packages/llvm-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json index 329fa308c..9fb682f0f 100644 --- a/packages/llvm-win32-x64-msvc/package.json +++ b/packages/llvm-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json index 75c54e9c3..362fbc572 100644 --- a/packages/runtime-android-arm64/package.json +++ b/packages/runtime-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-android-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for Android arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index 7473b05f3..a4176ed60 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index ab8666c88..91bc4182b 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-x64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json index a3a254685..2fdd68897 100644 --- a/packages/runtime-ios-arm64/package.json +++ b/packages/runtime-ios-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json index 7981422b8..cbdf96d1d 100644 --- a/packages/runtime-ios-simulator-arm64/package.json +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-simulator-arm64", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index d633ac303..e0d228925 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 460ca25fa..70eee223f 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index 9c6336a04..5822fd21d 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-gnu", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index e9f885a5c..1db34d4a7 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-musl", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json index c9a40c2c0..5bbd23755 100644 --- a/packages/runtime-wasm32-wasi/package.json +++ b/packages/runtime-wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-wasm32-wasi", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index f7e4c9860..df4a9a109 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-win32-x64-msvc", - "version": "0.4.1", + "version": "0.4.2", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1af249b9d..9232266af 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.4.1", + "version": "0.4.2", "description": "scriptc native runtime sources and vendored dependencies", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/threads/package.json b/packages/threads/package.json index 2aeeb7cbb..11b8ef624 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.4.1", + "version": "0.4.2", "description": "Zero-copy publication of immutable object graphs to worker threads (scriptc), with a deep-freezing Node.js implementation", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/types/package.json b/packages/types/package.json index ffbbdf4a4..689c7b72f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.4.1", + "version": "0.4.2", "description": "Contract types for scriptc: type-only annotations (Int32, NoThrow, Immutable) that scriptc checks and optimizes, with no runtime semantics", "license": "Apache-2.0", "homepage": "https://scriptc.dev", From fd40a8c84a136cdeaa18b08d5cf4c566e32eadd7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:18:30 -0500 Subject: [PATCH 058/191] Try each PATH entry with exec when spawning with a replacement env The PATH lookup for spawns with an `env` option picked the first executable regular file and stopped there, so a script whose interpreter is missing ended the search with ENOENT. Node's libuv runs execvp in the child, which moves on to the next PATH entry when an exec fails with ENOENT, ENOTDIR or EACCES. Each candidate now runs through posix_spawn in order with the same rule, and relative entries are resolved by the child in its cwd. --- packages/runtime/src/scr_child.c | 76 +++++++--------------- tests/corpus/7036-child-env-path-lookup.ts | 15 ++++- 2 files changed, 36 insertions(+), 55 deletions(-) diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index c1d98b35b..df7543500 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2907,7 +2907,6 @@ void scr_children_poll(void) { #include #include #include -#include #include #include #include @@ -3112,21 +3111,23 @@ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status, static char **scr_exec_envp(ScrArr *pairs); static void scr_exec_envp_free(char **envp); -/* The executable `file` names when `envp` replaces the child's - * environment, found as libuv finds it: a name with a '/' is used as - * given; otherwise each entry of the replacement's PATH (an empty entry - * is the current directory), or of the default path when the replacement - * has no PATH, is tried in order. Relative candidates are checked from - * `cwd`, the directory the child starts in (NULL: this process's). Returns - * a malloc'd path, or NULL with *err set: EACCES when a candidate exists - * but cannot run, else ENOENT. */ -static char *scr_spawn_path_lookup(const char *file, char *const *envp, const char *cwd, - int *err) { - if (strchr(file, '/')) { - char *copy = strdup(file); - if (!copy) scr_child_oom(); - return copy; - } +/* posix_spawnp for the child_process spawns. Without `envp` the command + * resolves on this process's PATH over `environ`. With `envp` (a + * replacement environment) it resolves the way Node's libuv runs execvp + * in the child: a name with a '/' runs as given; otherwise each entry of + * the replacement's PATH (an empty entry is the current directory), or of + * the default path when the replacement has no PATH, is tried in order, + * and a candidate whose exec fails with ENOENT, ENOTDIR or EACCES (a + * missing file, or a script whose interpreter is missing) moves on to the + * next entry. The last such failure is EACCES if any candidate was denied, + * else ENOENT. Relative entries are resolved by the child, which starts in + * `cwd`. Returns posix_spawn's error number. */ +static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, char *const *argv, + char *const *envp, const char *cwd) { + (void)cwd; /* the file actions change into it before the exec */ + if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ); + if (strchr(file, '/')) return posix_spawn(pid, file, fa, attr, argv, envp); const char *path = "/usr/bin:/bin"; for (char *const *e = envp; *e; e++) { if (strncmp(*e, "PATH=", 5) == 0) { @@ -3135,7 +3136,6 @@ static char *scr_spawn_path_lookup(const char *file, char *const *envp, const ch } } size_t file_len = strlen(file); - size_t cwd_len = cwd ? strlen(cwd) : 0; bool denied = false; for (const char *dir = path;;) { const char *end = strchr(dir, ':'); @@ -3147,51 +3147,19 @@ static char *scr_spawn_path_lookup(const char *file, char *const *envp, const ch memcpy(candidate, prefix, prefix_len); candidate[prefix_len] = '/'; memcpy(candidate + prefix_len + 1, file, file_len + 1); - char *checked = candidate; - if (cwd_len > 0 && candidate[0] != '/') { - checked = malloc(cwd_len + 1 + strlen(candidate) + 1); - if (!checked) scr_child_oom(); - memcpy(checked, cwd, cwd_len); - checked[cwd_len] = '/'; - strcpy(checked + cwd_len + 1, candidate); - } - struct stat st; - bool runs = false; - if (stat(checked, &st) == 0) { - runs = S_ISREG(st.st_mode) && access(checked, X_OK) == 0; - if (!runs) denied = true; - } else if (errno == EACCES) { - denied = true; - } - if (checked != candidate) free(checked); - if (runs) return candidate; + int err = posix_spawn(pid, candidate, fa, attr, argv, envp); free(candidate); + if (err == EACCES) denied = true; + else if (err != ENOENT && err != ENOTDIR) return err; if (!end) break; dir = end + 1; } - *err = denied ? EACCES : ENOENT; - return NULL; -} - -/* posix_spawnp for the child_process spawns: with `envp` (a replacement - * environment) the command resolves on that environment's PATH - * (scr_spawn_path_lookup) as Node's does; without it, on this process's - * PATH over `environ`. Returns posix_spawn's error number. */ -static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, - const posix_spawnattr_t *attr, char *const *argv, - char *const *envp, const char *cwd) { - if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ); - int err = 0; - char *resolved = scr_spawn_path_lookup(file, envp, cwd, &err); - if (!resolved) return err; - err = posix_spawn(pid, resolved, fa, attr, argv, envp); - free(resolved); - return err; + return denied ? EACCES : ENOENT; } /* env_pairs: NULL inherits the parent's environment; else [k,v,...] * replaces it, as Node's `env` option does, and the command is looked up - * on the replacement's PATH (scr_spawn_path_lookup). */ + * on the replacement's PATH (scr_spawn_command). */ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms, int killsig, int in_mode, int out_mode, int err_mode, ScrArr *env_pairs) { diff --git a/tests/corpus/7036-child-env-path-lookup.ts b/tests/corpus/7036-child-env-path-lookup.ts index cac0670e9..97c93d835 100644 --- a/tests/corpus/7036-child-env-path-lookup.ts +++ b/tests/corpus/7036-child-env-path-lookup.ts @@ -11,6 +11,12 @@ mkdirSync(join(dir, "tools")); writeFileSync(join(dir, "tools", "child-tool"), "#!/bin/sh\necho child-tool $1\n"); chmodSync(join(dir, "tools", "child-tool"), 0o755); +// An earlier PATH entry holding a script whose interpreter is missing +// moves the lookup on to the next entry, as execvp does. +mkdirSync(join(dir, "broken")); +writeFileSync(join(dir, "broken", "child-tool"), "#!/nonexistent/interpreter\n"); +chmodSync(join(dir, "broken", "child-tool"), 0o755); + const show = (e: unknown): string => { const err = e as NodeJS.ErrnoException; return `${err.code} ${err.message.replace(dir, "")}`; @@ -23,11 +29,18 @@ try { } catch (e) { console.log(show(e)); } +const twoEntries = `${join(dir, "broken")}:${join(dir, "tools")}`; +console.log(execFileSync("child-tool", ["fallthrough"], { env: { PATH: twoEntries }, encoding: "utf8" }).trim()); +try { + execFileSync("child-tool", [], { env: { PATH: join(dir, "broken") }, encoding: "utf8" }); +} catch (e) { + console.log(show(e)); +} console.log( execFileSync("child-tool", ["relative"], { env: { PATH: "tools" }, cwd: dir, encoding: "utf8" }).trim(), ); -const child = spawn("child-tool", ["async"], { env: { PATH: join(dir, "tools") }, stdio: ["ignore", "pipe", "ignore"] }); +const child = spawn("child-tool", ["async"], { env: { PATH: twoEntries }, stdio: ["ignore", "pipe", "ignore"] }); let out = ""; child.stdout!.on("data", (chunk: Buffer) => (out += chunk.toString())); child.on("close", (code) => { From 7cce36354f20204698523128fc3072b5c58ca5ad Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:21:10 -0500 Subject: [PATCH 059/191] 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 060/191] 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 061/191] 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 972e44a11a530321cdaf38aeeffc68b9639297e3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:40:01 -0500 Subject: [PATCH 062/191] Hold any later value in JavaScript properties initialized to undefined or null A JavaScript object literal's property whose initializer is `undefined` or `null` (`[{ oldPos: -1, lastComponent: undefined }]`, `{ user: null }`) mapped to the unit-only union, so a later write of another value threw: the diff package's diffLines failed with "a '{ added: boolean; count: number; previousComponent: unknown; removed: boolean }' value is not representable in the target union" when it stored its first path component. Nothing checks a JavaScript file's writes, so such a property is now checked-dynamic and holds whatever the program stores. TypeScript properties keep the unit-only union, and `__proto__: null` still sets the prototype. --- packages/compiler/src/frontend/type-mapper.ts | 23 ++++++++++ .../main.ts.json | 6 +++ .../main.ts | 8 ++++ .../node_modules/edit-paths/index.d.ts | 2 + .../node_modules/edit-paths/index.js | 43 +++++++++++++++++++ .../node_modules/edit-paths/package.json | 1 + .../main.ts.json | 1 + 7 files changed, 84 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json create mode 100644 tests/corpus/7008-js-unit-initialized-properties/main.ts create mode 100644 tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts create mode 100644 tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js create mode 100644 tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json create mode 100644 tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 93349054a..fa8897266 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -5744,6 +5744,28 @@ function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChe ); } +/** 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 + * property whose only initializer is a unit holds the checked-dynamic value + * rather than the unit-only union. */ +function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean { + if (t.kind !== "union") return false; + const def = ctx.unions.get(t.unionId); + return def !== undefined && def.arms.every((arm) => isUnitType(arm)); +} + +function jsObjectLiteralProperty(p: ts.Symbol): boolean { + const d = p.valueDeclaration; + // `__proto__: null` sets the literal's prototype; it declares no property. + return ( + d !== undefined && + (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) && + ts.symbolName(p) !== "__proto__" && + isJsSourceFile(ts.getSourceFileOfNode(d)) + ); +} + function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; @@ -7811,6 +7833,7 @@ function mapRecordTypeInner( // omits the undefined arm exactly like an omitted optional. if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); + if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) 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/7008-js-unit-initialized-properties/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..85f5a6e12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7008-js-unit-initialized-properties/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7008-js-unit-initialized-properties/main.ts b/tests/corpus/7008-js-unit-initialized-properties/main.ts new file mode 100644 index 000000000..3ac113d6a --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/main.ts @@ -0,0 +1,8 @@ +// A JavaScript package whose object literals start a property as +// `undefined` or `null` and later store records, numbers and arrays in it, +// as the diff package's path list does (`{ oldPos: -1, lastComponent: +// undefined }`). Node runs the later writes; each property holds them. +import { describe, session } from "edit-paths"; + +console.log(describe()); +console.log(session()); diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts new file mode 100644 index 000000000..68e2b409b --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.d.ts @@ -0,0 +1,2 @@ +export declare function describe(): string; +export declare function session(): string; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js new file mode 100644 index 000000000..ec3e7c4d0 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/index.js @@ -0,0 +1,43 @@ +"use strict"; +// A Myers-style path list, as the diff package keeps it: each path starts +// with `lastComponent: undefined` and later holds a linked list of records, +// and a state object starts with `null` fields that later hold other values. +function addToPath(path, added, removed, oldPosInc) { + var last = path.lastComponent; + if (last && last.added === added && last.removed === removed) { + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent } }; + } + return { oldPos: path.oldPos + oldPosInc, lastComponent: { count: 1, added: added, removed: removed, previousComponent: last } }; +} +function extractCommon(basePath, common) { + if (common) basePath.lastComponent = { count: common, previousComponent: basePath.lastComponent, added: false, removed: false }; + basePath.oldPos += common; +} +function components(lastComponent) { + var list = []; + while (lastComponent) { + list.push(lastComponent); + var next = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = next; + } + return list.reverse(); +} +function describe() { + var bestPath = [{ oldPos: -1, lastComponent: undefined }]; + extractCommon(bestPath[0], 1); + var path = addToPath(bestPath[0], false, true, 1); + path = addToPath(path, true, false, 0); + path = addToPath(path, true, false, 0); + return components(path.lastComponent).map(function (c) { + return (c.added ? "+" : c.removed ? "-" : "=") + c.count + ("previousComponent" in c ? "!" : ""); + }).join(" "); +} +function session() { + var state = { user: null, retries: null, tags: undefined }; + state.user = { name: "ada" }; + state.retries = 3; + state.tags = ["a", "b"]; + return state.user.name + " " + state.retries + " " + state.tags.join("+") + " " + JSON.stringify(state); +} +module.exports = { describe: describe, session: session }; diff --git a/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json new file mode 100644 index 000000000..98648c289 --- /dev/null +++ b/tests/corpus/7008-js-unit-initialized-properties/node_modules/edit-paths/package.json @@ -0,0 +1 @@ +{ "name": "edit-paths", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json new file mode 100644 index 000000000..3a6df58d2 --- /dev/null +++ b/tests/harness/corpus-costs/7008-js-unit-initialized-properties/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3065, "san": 2623 } From 6681b9407e79d09f4be6f9e64692db6bf961f3d0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:46:21 -0500 Subject: [PATCH 063/191] Version functions on number parameters that bound their loops A number parameter that every call site proves an int32 reaches the function's i32 entry. One that some caller passes as an unproven number, such as a value read from a worker message or parsed from text, kept its double, so every loop it bounds compared an integer counter with a double limit and the values derived from it stayed doubles. When such a parameter bounds a numeric loop (arithmetic, element and field reads and writes, Math calls, no allocation or other calls), the function now gets a copy of its body. Its own body first tests each such parameter for an int32 that is not -0 and, when all pass, calls the copy with `p | 0`, the same double. The copy's only caller proves those parameters int32s, so it gets the i32 entry and integer loops. Other values run the body as before. Only plain synchronous functions of at most 2048 IR nodes are versioned: not class members or generated helpers, not capturing, not the entry, not dynamic-tier, not calling themselves, and at most 16 per module. A function holding the only call site of another function is kept whole unless that callee only computes a number or boolean from scalar reads: a second call site can stop LLVM from inlining the callee and leave the records it builds in memory. Library builds and modules that observe call stacks are left as they are. --- packages/compiler/src/backend/llvm/emitter.ts | 49 ++- .../compiler/src/ir/parameter-versions.ts | 307 ++++++++++++++++++ .../test/parameter-version-emission.test.ts | 71 ++++ ...7323-loop-bound-parameter-versions.ts.json | 6 + ...4-loop-bound-parameter-window-sums.ts.json | 6 + .../7323-loop-bound-parameter-versions.ts | 88 +++++ .../7324-loop-bound-parameter-window-sums.ts | 33 ++ ...7323-loop-bound-parameter-versions.ts.json | 1 + ...4-loop-bound-parameter-window-sums.ts.json | 1 + 9 files changed, 544 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/src/ir/parameter-versions.ts create mode 100644 packages/compiler/test/parameter-version-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json create mode 100644 tests/corpus/7323-loop-bound-parameter-versions.ts create mode 100644 tests/corpus/7324-loop-bound-parameter-window-sums.ts create mode 100644 tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json create mode 100644 tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..19604f38c 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -72,6 +72,7 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { addRecordResultEntries } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"; +import { versionLoopBoundParameters } from "../../ir/parameter-versions.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"; @@ -442,6 +443,14 @@ export function lowRegionTarget(platform: string, sanitize: boolean): boolean { return platform === "linux" && !sanitize; } +/** Function versions on loop-bounding number parameters + * (ir/parameter-versions.ts). Library builds have no int32 entries, and a + * module that observes call stacks could see the version's frame. */ +function versionParameters(mod: IrModule): IrModule { + if (mod.lib !== undefined || observesStacks(mod)) return mod; + return versionLoopBoundParameters(mod, mangleFunction); +} + 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. @@ -449,20 +458,22 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) // Keep source storage intact for debugger inspection in dev builds. return new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ) @@ -481,20 +492,22 @@ export function emitLlvmModuleSource( const mod = unshareNodes(input); const parts = new LlEmitter( options.debugSources === undefined - ? addRecordResultEntries( - scalarizeNumericRecords( - specializeCallbackCalls( - specializeNumericCalls( - foldConstantGlobals( - copyStableCaptures( - resolveSelfCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ? versionParameters( + addRecordResultEntries( + scalarizeNumericRecords( + specializeCallbackCalls( + specializeNumericCalls( + foldConstantGlobals( + copyStableCaptures( + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), + mangleFunction, ), mangleFunction, ), - mangleFunction, ), ), ) diff --git a/packages/compiler/src/ir/parameter-versions.ts b/packages/compiler/src/ir/parameter-versions.ts new file mode 100644 index 000000000..88232251c --- /dev/null +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -0,0 +1,307 @@ +import { analyzeInt32Slots } from "./int32-slots.js"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; +import { reparseStmt } from "./serialize.js"; +import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js"; + +const MAX_FUNCTION_NODES = 2048; +const MAX_VERSIONS = 16; + +function size(fn: IrFunction): number { + let nodes = 0; + everyStmtList(fn.body, { + stmt: () => ++nodes <= MAX_FUNCTION_NODES, + expr: () => ++nodes <= MAX_FUNCTION_NODES, + }); + return nodes; +} + +/** The function a direct call or construction runs, if any. */ +function callee(e: IrExpr): string | null { + if (e.kind === "call") return e.callee; + if (e.kind === "new") return `%${e.className}.constructor`; + return null; +} + +/** Direct call and construction sites of each function in the module. */ +function callSites(mod: IrModule): Map { + const sites = new Map(); + for (const fn of mod.functions) + everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name !== null) sites.set(name, (sites.get(name) ?? 0) + 1); + return true; + }, + }); + return sites; +} + +/** Expressions and statements of a callee that LLVM inlines at any number + * of call sites: scalar arithmetic and reads, no calls or allocations. */ +const SIMPLE_EXPRESSIONS = new Set([ + "varRef", + "numLit", + "boolLit", + "bin", + "unary", + "logical", + "ternary", + "toBool", + "incDec", + "assignExpr", + "arrayGet", + "fieldGet", + "recordGet", + "unionNarrow", + "unionIsTag", +]); +const SIMPLE_STATEMENTS = new Set([ + "varDecl", + "assign", + "exprStmt", + "if", + "block", + "return", +]); + +function simple(fn: IrFunction): boolean { + return ( + (fn.returnType.kind === "f64" || fn.returnType.kind === "bool") && + everyStmtList(fn.body, { + stmt: (s) => SIMPLE_STATEMENTS.has(s.kind), + expr: (e) => SIMPLE_EXPRESSIONS.has(e.kind), + }) + ); +} + +/** Whether a copy of `fn` would cost a callee its only call site. LLVM + * inlines a function called once wherever its size allows; with a second + * site, both calls can stay out of line, with the records and arguments + * they pass built in memory. A callee that only computes a number or a + * boolean from scalar reads inlines either way. A function calling itself + * would also dispatch on every call. */ +function keepsSingleBody( + fn: IrFunction, + sites: ReadonlyMap, + functions: ReadonlyMap, +): boolean { + return !everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + const name = callee(e); + if (name === null) return true; + if (name === fn.name) return false; + if (sites.get(name) !== 1) return true; + const target = functions.get(name); + return target !== undefined && simple(target); + }, + }); +} + +/** Statements and expressions of a loop whose work is numeric: arithmetic, + * element and field reads and writes, `Math` calls and calls of simple + * functions. Loops that allocate, build strings or call other code gain + * little from an integer bound and would only grow the copy. */ +const NUMERIC_STATEMENTS = new Set([ + ...SIMPLE_STATEMENTS, + "for", + "while", + "doWhile", + "break", + "continue", + "arraySet", + "fieldSet", +]); +const NUMERIC_EXPRESSIONS = new Set([...SIMPLE_EXPRESSIONS, "arrayHas", "arrayState"]); + +function numericLoop(loop: IrStmt, functions: ReadonlyMap): boolean { + const expr = (e: IrExpr): boolean => { + let allowed = NUMERIC_EXPRESSIONS.has(e.kind); + if (e.kind === "call") { + const target = functions.get(e.callee); + allowed = target !== undefined && simple(target); + } else if (e.kind === "libCall") allowed = e.fn.startsWith("math."); + else if (e.kind === "arrIntrinsic") allowed = e.method === "length"; + return allowed && everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => + NUMERIC_STATEMENTS.has(s.kind) && everyStmtChild(s, expr, stmt); + return stmt(loop); +} + +/** Number parameters of `fn` that bound one of its numeric loops: read in + * the loop's condition or in its `for` initializer. */ +function loopBounds(fn: IrFunction, functions: ReadonlyMap): Set { + const params = new Set( + fn.params.filter((param) => param.type.kind === "f64").map((param) => param.localId), + ); + const result = new Set(); + if (params.size === 0) return result; + const reads = (e: IrExpr): boolean => { + if (e.kind === "varRef" && params.has(e.localId)) result.add(e.localId); + return everyExprChild(e, reads, readsStmt); + }; + const readsStmt = (s: IrStmt): boolean => everyStmtChild(s, reads, readsStmt); + everyStmtList(fn.body, { + stmt: (s) => { + if (s.kind !== "for" && s.kind !== "while" && s.kind !== "doWhile") return true; + if (!numericLoop(s, functions)) return true; + if (s.cond) reads(s.cond); + if (s.kind === "for" && s.init) readsStmt(s.init); + return true; + }, + expr: () => true, + }); + return result; +} + +/** Versions of functions whose number parameters bound their loops. + * + * A parameter that every call site proves an int32 already reaches the + * function's i32 entry (int32-abi.ts). One that some caller passes as an + * unproven number (a value read from a message, a field, a parse) keeps + * its double, so the loops it bounds compare a counter with a double and + * the values derived from it stay doubles. + * + * A parameter qualifies when it bounds a numeric loop (`numericLoop`). + * Such a function gets a copy of its body under a new name, and its own + * body starts by calling the copy when every such parameter holds an + * int32: `(p | 0) === p` (an integer in the int32 range, not NaN) and not + * -0 (`1 / p` is not -Infinity). The copy then receives `p | 0`, which is + * the same double, so it computes what the body would; its only caller + * proves the parameter an int32, so it gets the i32 entry. Other values + * run the body as before. + * + * Only plain synchronous functions qualify: not class members, no + * captures, not the module entry, not dynamic-tier, at most + * MAX_FUNCTION_NODES nodes, not calling themselves, and not holding the + * only call site of another function (`keepsSingleBody`). The caller + * skips modules that observe call stacks, where the copy's frame could be + * seen. */ +export function versionLoopBoundParameters( + mod: IrModule, + symbolName: (name: string) => string = (name) => name, +): IrModule { + const candidates: { fn: IrFunction; params: Set }[] = []; + const sites = callSites(mod); + const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); + const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? [])); + for (const fn of mod.functions) { + if ( + // Constructors, methods and accessors (`%C.m`) and generated helper + // families (`%arr.map_…`) keep their single body. + fn.name.includes(".") || + members.has(fn.name) || + fn.async || + fn.generator || + fn.captures !== undefined || + fn.classCaptures?.length || + fn.tier === "dynamic" || + fn.name === mod.entry + ) + continue; + const params = loopBounds(fn, functions); + const locals = new Map(fn.locals.map((local) => [local.id, local])); + for (const id of params) { + const local = locals.get(id); + if (!local || local.boxed || local.tdz) params.delete(id); + } + if (params.size === 0 || size(fn) > MAX_FUNCTION_NODES || keepsSingleBody(fn, sites, functions)) + continue; + candidates.push({ fn, params }); + } + if (candidates.length === 0) return mod; + const slots = analyzeInt32Slots(mod); + const names = new Set(mod.functions.map((fn) => symbolName(fn.name))); + const replaced = new Map(); + const added: IrFunction[] = []; + for (const { fn, params } of candidates) { + if (added.length >= MAX_VERSIONS) break; + const unproven = fn.params.filter( + (param) => params.has(param.localId) && !slots.hasParam(fn.name, param.localId), + ); + if (unproven.length === 0) continue; + let name = `${fn.name}.int32Params`; + while (names.has(symbolName(name))) name += "_"; + names.add(symbolName(name)); + const { ownsPrototype: _ownsPrototype, ...implementation } = fn; + added.push({ ...implementation, name, body: fn.body.map(reparseStmt) }); + const loc = fn.body[0]?.loc ?? { file: mod.sourceFile, start: 0, end: 0 }; + const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); + const int32 = (localId: string): IrExpr => ({ + kind: "bin", + op: "|", + left: read(localId), + right: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }); + let test: IrExpr | null = null; + for (const param of unproven) { + const exact: IrExpr = { + kind: "bin", + op: "===", + left: int32(param.localId), + right: read(param.localId), + type: BOOL, + loc, + }; + const notNegativeZero: IrExpr = { + kind: "bin", + op: "!==", + left: { + kind: "bin", + op: "/", + left: { kind: "numLit", value: 1, type: F64, loc }, + right: read(param.localId), + type: F64, + loc, + }, + right: { kind: "numLit", value: -Infinity, type: F64, loc }, + type: BOOL, + loc, + }; + const both: IrExpr = { + kind: "logical", + op: "&&", + left: exact, + right: notNegativeZero, + type: BOOL, + loc, + }; + test = + test === null + ? both + : { kind: "logical", op: "&&", left: test, right: both, type: BOOL, loc }; + } + const versioned = new Set(unproven.map((param) => param.localId)); + const call: IrExpr = { + kind: "call", + callee: name, + args: fn.params.map((param) => + versioned.has(param.localId) + ? int32(param.localId) + : { kind: "varRef", localId: param.localId, type: param.type, loc }, + ), + type: fn.returnType, + loc, + }; + const run: IrStmt[] = + fn.returnType.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }]; + replaced.set(fn, { + ...fn, + body: [{ kind: "if", cond: test!, then: run, else_: null, loc }, ...fn.body], + }); + } + if (added.length === 0) return mod; + return { + ...mod, + functions: [...mod.functions.map((fn) => replaced.get(fn) ?? fn), ...added], + }; +} diff --git a/packages/compiler/test/parameter-version-emission.test.ts b/packages/compiler/test/parameter-version-emission.test.ts new file mode 100644 index 000000000..eb6daff56 --- /dev/null +++ b/packages/compiler/test/parameter-version-emission.test.ts @@ -0,0 +1,71 @@ +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-parameter-version-")); + 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 versions = (llvm: string): string[] => [ + ...new Set([...llvm.matchAll(/^define [^@]*@sc_\w+?_(\w+?)_int32Params\b/gm)].map((m) => m[1]!)), +]; + +const loops = ` +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function literal(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} +function recursive(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return n > 0 ? s + recursive(n - 1) : s; +} +function unbounded(n: number): number { + let s = 0; + for (let i = 0; i < 10; i++) s += n; + return s; +} +class Counter { + total(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; + } +} +const n = Number(process.argv.length > 5 ? "7.5" : "6"); +console.log(sumTo(n), literal(4), literal(5), recursive(n), unbounded(n), new Counter().total(n)); +`; + +test("a loop-bounding parameter some caller passes unproven gets an int32 version", async () => { + const llvm = await emitLlvm(loops); + expect(versions(llvm)).toEqual(["sumTo"]); + // The version takes the parameter as an i32, and the body dispatches to it + // after testing for an int32 that is not -0. + expect(llvm).toMatch(/^define [^@]*@sc_\w+_sumTo_int32Params\.i32\(i32 /m); + const body = /^define [^@]*@sc_\w+?_sumTo\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).toMatch(/call [^@]*@sc_\w+_sumTo_int32Params/); + expect(body).toMatch(/fdiv double 0x3FF0000000000000, %\S+/); +}); + +test("a module that reads stacks keeps a single body", async () => { + const llvm = await emitLlvm(`${loops}\nconsole.log(typeof new Error("x").stack);\n`); + expect(versions(llvm)).toEqual([]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..0a79c8cdb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7323-loop-bound-parameter-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..aedd853fb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7324-loop-bound-parameter-window-sums.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7323-loop-bound-parameter-versions.ts b/tests/corpus/7323-loop-bound-parameter-versions.ts new file mode 100644 index 000000000..cdbb8cfc4 --- /dev/null +++ b/tests/corpus/7323-loop-bound-parameter-versions.ts @@ -0,0 +1,88 @@ +// Functions whose number parameters bound their loops, called with values +// no caller proves to be int32s: integers, fractions, NaN, -0, infinities, +// and integers past the int32 range. Every call prints Node's result, +// including -0 read back through 1 / n, a reassigned parameter, a thrown +// error, a void result and a function passed as a value. + +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function sumTo(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += i; + return s; +} + +function fill(n: number, out: number[]): void { + for (let i = 0; i < n; i++) out.push(i * 2); +} + +function sign(n: number): number { + let steps = 0; + for (let i = 0; i < n && steps < 4; i++) steps++; + return 1 / n + steps; +} + +function countdown(n: number, step: number): number { + let k = 0; + while (n > 0) { + n -= step; + k++; + if (k > 100) break; + } + return k * 1000 + n; +} + +function checked(n: number, limit: number): number { + let s = 0; + let over = -1; + for (let i = 0; i <= n; i++) { + if (i > limit && over < 0) over = i; + s += i; + } + if (over >= 0) throw new RangeError(`past ${limit} at ${over}`); + return s; +} + +function fillTo(n: number, out: number[]): number { + for (let i = 0; i < n; i++) out[i] = i * 3; + return out.length; +} + +function window2(a: number[], from: number, to: number): string { + const parts: string[] = []; + for (let i = from; i < to; i++) parts.push(String(a[i])); + return parts.join(","); +} + +// Values arrive as strings and JSON, so no caller proves them int32s. +const inputs: number[] = JSON.parse("[0, 1, 5, 7.5, -3, -0, 2147483647, 2147483648, -2147483649, 1e300]"); +inputs.push(NaN, Infinity, -Infinity, Number("12"), Number.parseFloat("3.25")); + +for (const n of inputs) { + const small = Math.abs(n) > 100 ? (n > 0 ? 3 : -3) : n; + show(`sumTo ${n}`, sumTo(small)); + show(`sign ${n}`, sign(n)); + show(`countdown ${n}`, countdown(small, 2)); + const out: number[] = []; + fill(small, out); + const dense: number[] = []; + const length = fillTo(small, dense); + console.log(`fill ${n}`, out.join(" "), length, dense.join(" ")); +} + +for (const n of [3, 4.5, 6]) { + try { + show(`checked ${n}`, checked(n, 4)); + } catch (e) { + console.log(`checked ${n} threw`, (e as Error).message); + } +} + +const data = [10, 20, 30, 40, 50]; +for (const [from, to] of JSON.parse("[[0, 2], [1.5, 4], [-1, 1], [3, 9], [2, 2]]") as [number, number][]) + console.log(`window ${from}..${to}`, window2(data, from, to)); + +const fns: ((n: number) => number)[] = [sumTo, (n) => sumTo(n) * 2]; +console.log(fns.map((f) => f(Number("4"))).join(" "), [1, 2.5, 3].map(sumTo).join(" ")); diff --git a/tests/corpus/7324-loop-bound-parameter-window-sums.ts b/tests/corpus/7324-loop-bound-parameter-window-sums.ts new file mode 100644 index 000000000..094e9848c --- /dev/null +++ b/tests/corpus/7324-loop-bound-parameter-window-sums.ts @@ -0,0 +1,33 @@ +// Sliding-window sums whose length and width are number parameters read +// from JSON, so no caller proves them int32s: int32 sizes run the +// function's int32 version, and a fractional width, a length past the +// int32 range, -0 and NaN run its ordinary body. + +function windowSums(n: number, width: number, values: number[], out: number[]): number { + let total = 0; + for (let i = 0; i + width <= n; i++) { + let s = 0; + for (let k = 0; k < width; k++) s += values[i + k]!; + out[i] = s; + total += s; + } + return total; +} + +const values: number[] = []; +for (let i = 0; i < 4000; i++) values.push((i * 37) % 101); + +const runs = JSON.parse( + "[[4000, 64, 3], [4000, 1, 2], [100, 2.5, 2], [10, 0, 1], [-0, 3, 1], [4000, 3000000000, 1], [3000000000, 4, 1], [12, 20, 1]]", +) as [number, number, number][]; +runs.push([NaN, 2, 1], [Number("250"), Number("8"), 2]); +for (const [n, width, rounds] of runs) { + const out: number[] = []; + const length = Math.min(Number.isFinite(n) ? n : 0, 4000); + for (let i = 0; i < length; i++) out.push(0); + let total = 0; + // A length past the values reads holes: keep that run within them. + const limit = n > 4000 ? 4000 : n; + for (let r = 0; r < rounds; r++) total += windowSums(limit, width, values, out); + console.log(`${n} ${width} ${rounds}:`, total, out.length, out.slice(0, 3).join(",")); +} diff --git a/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json new file mode 100644 index 000000000..ee18f5f11 --- /dev/null +++ b/tests/harness/corpus-costs/7323-loop-bound-parameter-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2800 } diff --git a/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/7324-loop-bound-parameter-window-sums.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } From 7ed4597407f8aa8ab7496a4003c0fc517af71a04 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:59:49 -0500 Subject: [PATCH 064/191] 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 065/191] 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 066/191] 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 067/191] 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 068/191] 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 b4a3bace6005db55355e9c9e98108ffa9b60310b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:08:42 -0500 Subject: [PATCH 069/191] Read compact array slots in dense loop fast copies, and their flag only on 64-bit targets --- .../src/backend/llvm/local-array-reads.ts | 12 ++++++++++++ packages/runtime/src/scr_array.c | 2 +- packages/runtime/src/scr_array.h | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 223d45540..716d05a94 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -619,6 +619,18 @@ export function emitBorrowedArrayRead( ); host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + if (referenceLoad(host, read.arr.type) === "cref") { + // Compact references: 4-byte slots holding the address over 8. + const ref = B.tmp(); + B.line( + `${valuePtr} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${loopIndex}`, + ); + host.markMemoryPointer(valuePtr, "array:elements"); + B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); + for (const line of compactRefLoadLines(ref, value, `${value}.r`)) B.line(line.trimStart()); + B.line(`store ptr ${value}, ptr ${slot}`); + return; + } B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${loopIndex}`); host.markMemoryPointer(valuePtr, "array:elements"); B.line(`${value} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 50b461614..d4cdf0810 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -1417,7 +1417,7 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool small) { - if (__builtin_expect((ops->flags & SCR_ARR_OPS_COMPACT) != 0, 0)) + if (__builtin_expect(scr_arr_ops_compact(ops), 0)) return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF); return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 2fea1ef2c..67ff7d528 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -68,7 +68,9 @@ typedef struct ScrArrOps { /* Registry id (scr_arr_ops_table), 0 until scr_arr_ops_register assigns * it on the first array of the type. Descriptors are writable for it. */ uint32_t id; - /* SCR_ARR_OPS_* */ + /* SCR_ARR_OPS_*. On 64-bit targets the field takes the struct's former + * tail padding, so descriptors emitted without it read 0 here; on 32-bit + * targets it lies past their end and is never read (scr_arr_ops_compact). */ uint32_t flags; } ScrArrOps; @@ -76,6 +78,17 @@ typedef struct ScrArrOps { * arrays of the type are SCR_ELEM_CREF (llvm/shapes.ts arrOps). */ #define SCR_ARR_OPS_COMPACT 1u +/* Whether `ops` describes compact class elements. Compact instances exist + * only on 64-bit targets. */ +static inline bool scr_arr_ops_compact(const ScrArrOps *ops) { +#if UINTPTR_MAX > 0xffffffffu + return (ops->flags & SCR_ARR_OPS_COMPACT) != 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); From 8f08cbbec594caa07af7164bb57882209f81fbcd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:09:01 -0500 Subject: [PATCH 070/191] Build Sets, Maps and weak collections from checked-dynamic iterables `new Set(x)`, `new Map(x)`, `new WeakSet(x)`, `new WeakMap(x)` and `Object.fromEntries(x)` now accept any checked-dynamic iterable, as in Node: a native iterator (`map.keys()`, `set.values()`), a compiled generator, an object with a Symbol.iterator method, or a Set. Before, a Set seed that was not an array or a string threw "is not iterable", and a Map, weak collection or Object.fromEntries seed that was a generator or a collection other than a Map refused with SC2020. The runtime drains native iterators, packs arrays, strings and collections, and steps every other iterable through its iterator (scr_dyn_seed_values). A program that builds these from an untyped seed registers the compiled generators' next and return methods with the runtime, as destructuring rest already did, so a generator seed can be stepped. The seed's values are all taken before the first one is added, so when an entry of a Map seed is not an object the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry. The limitations page says so. --- .../limitations/builtin-apis/map-and-set.mdx | 2 ++ .../src/frontend/lowering/dispatch-sites.ts | 14 +++++++- .../6926-dynamic-collection-seeds.mjs.json | 6 ++++ .../6927-dynamic-collection-seeds.ts.json | 6 ++++ packages/runtime/src/scr_json.c | 32 +++++++++++-------- .../corpus/6926-dynamic-collection-seeds.mjs | 28 ++++++++++++++++ tests/corpus/6927-dynamic-collection-seeds.ts | 9 ++++++ .../6926-dynamic-collection-seeds.mjs.json | 1 + .../6927-dynamic-collection-seeds.ts.json | 1 + 9 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json create mode 100644 tests/corpus/6926-dynamic-collection-seeds.mjs create mode 100644 tests/corpus/6927-dynamic-collection-seeds.ts create mode 100644 tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json create mode 100644 tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index bf77001ee..ee7435b2d 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -7,4 +7,6 @@ They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. + Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one. + When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index 2618dacf9..c8a8d6ba6 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -11,6 +11,18 @@ import { everyStmtList } from "../../ir/traverse.js"; 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. */ +const RUNTIME_STEP_LIB_FNS: ReadonlySet = new Set([ + "dyn.destructureRest", + "dyn.nativeSetNew", + "dyn.mapSeedEntries", + "dyn.fromEntries", + "weakMap.new", + "weakSet.new", +]); + /** What one function's body holds for the class dispatch pass and its * caught-value, generator and class construction sub-passes, collected in * one walk. Lists keep the order the walk meets their entries. */ @@ -186,7 +198,7 @@ function scanSites(statements: IrStmt[]): FunctionSites { expr.key.kind === "strLit" && HELPER_METHODS.has(expr.key.value)) || expr.kind === "dynIterN" || - (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + (expr.kind === "libCall" && RUNTIME_STEP_LIB_FNS.has(expr.fn)) ) sites.runtimeSteps = true; switch (expr.kind) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..32edfc402 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6926-dynamic-collection-seeds.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..89c591dd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6927-dynamic-collection-seeds.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 1c85a928a..e4b66990d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1577,6 +1577,20 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); +/* The values a collection constructor's iterable seed gives, in order: +1 + * array, or NULL with the exception pending. A native iterator (a Map's + * keys(), a Set's values()) drains, which runs no program code; arrays, + * strings, typed arrays and collections pack; other iterables (a + * generator, an object with a Symbol.iterator method) step their + * iterator to the end, so their values are all taken before the + * constructor adds the first one. */ +static ScrDyn *scr_dyn_seed_values(const ScrDyn *src) { + if (src->kind == SCR_DYN_HANDLE && + (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) + return scr_native_iterator_drain(src); + return scr_dyn_destructure_rest(src, NULL); +} + /* The for-of-over-dyn pack accessors: the emitted index loop drives them * over a scr_dyn_iter_pack result (ARR by construction — the defensive * arms cover nothing reachable from that lowering). Never throw. */ @@ -1592,18 +1606,10 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) { if (scr_exc_pending()) return NULL; } /* A native iterator (sp.entries(), a Map's entries()) or a handle that - * steps through live entries (URLSearchParams): producing its entries - * runs no program code, so draining them first is what the consumer - * observes. */ - if (src->kind == SCR_DYN_HANDLE && - (src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step)) - return scr_native_iterator_drain(src); - if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) { - static const char message[] = "new Map(entries) over native non-array iterables has no lowering"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return NULL; - } - return scr_dyn_iter_pack(src, NULL); + * steps through live entries (URLSearchParams) drains; a Set, a + * generator or another iterable gives its values in order + * (scr_dyn_seed_values). */ + return scr_dyn_seed_values(src); } ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry) { @@ -14836,7 +14842,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) { ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed) { ScrDyn *items = NULL; if (seed->kind != SCR_DYN_NULL && seed->kind != SCR_DYN_UNDEF) { - items = scr_dyn_iter_pack(seed, NULL); + items = scr_dyn_seed_values(seed); if (!items) return NULL; } ScrMap *set = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_F64, &scr_map_dyn_set_ops); diff --git a/tests/corpus/6926-dynamic-collection-seeds.mjs b/tests/corpus/6926-dynamic-collection-seeds.mjs new file mode 100644 index 000000000..a48fab894 --- /dev/null +++ b/tests/corpus/6926-dynamic-collection-seeds.mjs @@ -0,0 +1,28 @@ +// Set, Map, WeakSet and Object.fromEntries over checked-dynamic iterables: +// native iterators, generators, objects with a Symbol.iterator method and +// collections, as seeds that reach the runtime untyped. +function* pairs() { yield ["g", 1]; yield ["h", 2]; } +function* values() { yield 1; yield 2; yield 2; } +const iterable = { *[Symbol.iterator]() { yield ["i", 3]; } }; +const valueIterable = { *[Symbol.iterator]() { yield "a"; yield "b"; } }; +function attempt(label, make) { + try { console.log(label, make()); } catch (e) { console.log(label, "threw", e.name, e.message); } +} +function viaSet(x) { return new Set(x).size; } +function viaMap(x) { return [...new Map(x).keys()].join(); } +function viaWeakSet(x) { return new WeakSet(x).has(key); } +const key = {}; +const m = new Map([["a", 1], ["b", 2]]); +attempt("set keys()", () => viaSet(m.keys())); +attempt("set values()", () => viaSet(new Set([1, 2]).values())); +attempt("set generator", () => viaSet(values())); +attempt("set iterable", () => viaSet(valueIterable)); +attempt("set string", () => viaSet("abca")); +attempt("set number", () => viaSet(5)); +attempt("map generator", () => viaMap(pairs())); +attempt("map iterable", () => viaMap(iterable)); +attempt("map set", () => viaMap(new Set([["s", 1]]))); +attempt("map entries()", () => viaMap(m.entries())); +attempt("map bad entry", () => viaMap([1])); +attempt("weakset generator", () => viaWeakSet((function* () { yield key; })())); +attempt("fromEntries generator", () => JSON.stringify(Object.fromEntries(pairs()))); diff --git a/tests/corpus/6927-dynamic-collection-seeds.ts b/tests/corpus/6927-dynamic-collection-seeds.ts new file mode 100644 index 000000000..c57433d57 --- /dev/null +++ b/tests/corpus/6927-dynamic-collection-seeds.ts @@ -0,0 +1,9 @@ +// Set and Map seeds held as any: native iterators and generators. +function* values(): Generator { yield 1; yield 2; yield 2; } +function* pairs(): Generator<[string, number]> { yield ["a", 1]; yield ["b", 2]; } +const m = new Map([["x", 1], ["y", 2]]); +const anyKeys: any = m.keys(); +const anyGen: any = values(); +const anyPairs: any = pairs(); +console.log(new Set(anyKeys).size, new Set(anyGen).size, new Map(anyPairs).size); +console.log(new Set(values()).size, new Map(pairs()).get("b"), new Set(m.values()).size); diff --git a/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json new file mode 100644 index 000000000..b4a56f5c5 --- /dev/null +++ b/tests/harness/corpus-costs/6926-dynamic-collection-seeds.mjs.json @@ -0,0 +1 @@ +{ "plain": 3813, "san": 3374 } diff --git a/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json new file mode 100644 index 000000000..93da087d2 --- /dev/null +++ b/tests/harness/corpus-costs/6927-dynamic-collection-seeds.ts.json @@ -0,0 +1 @@ +{ "plain": 2436, "san": 2273 } From dc9b60656d010567bb47c6c4c4f4ba14a3254f4e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:11:34 -0500 Subject: [PATCH 071/191] 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 e39ddf3d00e0b1305f4ceff4534547adace32f5c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:18:04 -0500 Subject: [PATCH 072/191] Refresh the native compiler's optimization profile --- packages/compiler/native/cli.profdata.gz | Bin 1452782 -> 1634955 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index 55523e76548ae821bbce8762876d84a3b3ccd182..e2f344a4d7f73f6e5b70e119a45c1e9db5fdf6d3 100644 GIT binary patch literal 1634955 zcmV);K!(2`iwFP!000021MD4loD)TNcXOTIj*fIt+V%oS@6tS~AcifocY$1TBst&& zDHe)~pjc2qu`8gWf`Vc}u>gvQilC@`B8s9YidX>oejz(|mq{k&3LKF4&&zCfr@ndf z-kUdX&N%ZtZqqTh3UL-l0)dU2-VI8>ziDSsdY6?>vC`Eoe1|iDJEYV)iyvwE(TL*s z*W)Y^d79ps(FE5uTS{q?EA^q^Bz@fXK|alXKEEM_rTJmvpBWu zYiFC()OtRrL7;~Boq8_Sk45Ww(}&L{i8~uSc=kiP_595?>1Xl1N$n{=UTZy{_-w{m zJTr6GSv(F_RHz*2ukVMS#Vu`C{a+3A?9I6~&*J|A*%H6as(KcW&kxkMVco!Ux?#1m z_`Ugo^zcyuJhofl`IfXm`tPp-cz@5p^Uthm>|PS6kk~8myteiHM8C7mAi7)6t?Chz zexa;^r-&}o+xh;N)XMtjur;J!-L9E-`*c>Y=kk(hcekbNKWeyROks zkL&om?XHH@KH}U$8gYK0q~&Iu<9cQg3Am7+^sqc2*J;ft5LlQ)EGuIjUN_k$uMfF9T-X z%oAAgcRfdjErKGFiJDWl9{u(|Y!RFyFbd0X94*s~$jWFDcUKBvfN4cCv2a9#rGNj@ z5(-B|$Wov0a=2Y~m+JAjr`dT^Vf;;z04qH4&e-WQ4sR^vL=bL~&drKT5-G%8zy18d z)EdnQB1mX0R(sXsv4g3=Q(!k`lVwkBr`*&?kZGb6aXB4isvxHm^_ubAi z;-Y9i^@_>Q#c9k&q!Irf`st>A^XuU#OAtj-Vp)#mC7D-3XgALi8?V9)P8!kPcU*sd z;bm6(H!_?I$=Ig>EeoiIsyV@Mr|N3Atv|zL-CMQu1;#B>qC8JYFjNeg-1tfLZJQcF zYtEWLQPvctv9wwf}op+%Tu>-=w9& z5sz9p)xuq2x^8%!UE;ddB8Cngec0I+)^$PHx^e;wc!6Vq%*lrijXlw+{#(gaO4v_H zaX!b_y*5!}YC9l)+B#IRDUvAd4sU

#6I&>venj0Nts2RG-7`%FelOZSP`g&X}ef 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Ȑt9k6i}dF2La)|6;2iU-|VDt;+U@FU^I@MnkNW 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

0n8;PO+|GJL< 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%0DsH7X?j`A@W!=`W+JG4VWYUC`O4pK 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%0A3fH>pQsO`t<97)sV;h2r8 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

S7zDpT)IviIZ-pYd?7tTuPrKUWEbWyYr>u!Tt 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�k=enncIpVse{ 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�*Er089Uq%U)VgzeO;q@MU4O56u0EQv@f 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 z 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�QO7wMev~+5%^H*C8Aiz%MXUy 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�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